Dev -> Local #14

Merged
cpfarhood merged 40 commits from dev into local 2026-05-16 14:16:30 +00:00
5 changed files with 379 additions and 17 deletions
Showing only changes of commit c445e59256 - Show all commits
+4
View File
@@ -399,6 +399,10 @@ export interface IssueComment {
authorType: IssueCommentAuthorType;
authorAgentId: string | null;
authorUserId: string | null;
createdByRunId?: string | null;
derivedAuthorAgentId?: string | null;
derivedCreatedByRunId?: string | null;
derivedAuthorSource?: "run_log_comment_post" | null;
body: string;
presentation: IssueCommentPresentation | null;
metadata: IssueCommentMetadata | null;
+69 -1
View File
@@ -24,7 +24,12 @@ import {
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import { instanceSettingsService } from "../services/instance-settings.ts";
import { clampIssueListLimit, ISSUE_LIST_MAX_LIMIT, issueService } from "../services/issues.ts";
import {
clampIssueListLimit,
deriveIssueCommentRunLogAttribution,
ISSUE_LIST_MAX_LIMIT,
issueService,
} from "../services/issues.ts";
import { buildProjectMentionHref, MAX_ISSUE_REQUEST_DEPTH } from "@paperclipai/shared";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
@@ -38,6 +43,69 @@ describe("issue list limit helpers", () => {
});
});
describe("deriveIssueCommentRunLogAttribution", () => {
it("recovers agent attribution from run logs that printed the posted comment id", () => {
const commentId = randomUUID();
const runId = randomUUID();
const agentId = randomUUID();
const derived = deriveIssueCommentRunLogAttribution(
[
{
id: commentId,
authorAgentId: null,
authorUserId: "user-1",
createdByRunId: null,
createdAt: new Date("2026-05-11T18:55:40.090Z"),
},
],
[
{
runId,
agentId,
createdAt: new Date("2026-05-11T18:51:56.246Z"),
startedAt: new Date("2026-05-11T18:51:56.257Z"),
finishedAt: new Date("2026-05-11T18:55:45.600Z"),
logContent: `comment id: ${commentId}\n`,
},
],
);
expect(derived.get(commentId)).toEqual({
derivedAuthorAgentId: agentId,
derivedCreatedByRunId: runId,
derivedAuthorSource: "run_log_comment_post",
});
});
it("does not rewrite comments without exact run-log proof", () => {
const commentId = randomUUID();
const derived = deriveIssueCommentRunLogAttribution(
[
{
id: commentId,
authorAgentId: null,
authorUserId: "user-1",
createdByRunId: null,
createdAt: new Date("2026-05-11T18:55:40.090Z"),
},
],
[
{
runId: randomUUID(),
agentId: randomUUID(),
createdAt: new Date("2026-05-11T18:51:56.246Z"),
startedAt: new Date("2026-05-11T18:51:56.257Z"),
finishedAt: new Date("2026-05-11T18:55:45.600Z"),
logContent: "posted results without echoing the comment id",
},
],
);
expect(derived.has(commentId)).toBe(false);
});
});
async function ensureIssueRelationsTable(db: ReturnType<typeof createDb>) {
await db.execute(sql.raw(`
CREATE TABLE IF NOT EXISTS "issue_relations" (
+213 -8
View File
@@ -60,6 +60,7 @@ import { buildInitialIssueMonitorFields, normalizeIssueExecutionPolicy } from ".
import { instanceSettingsService } from "./instance-settings.js";
import { redactCurrentUserText } from "../log-redaction.js";
import { resolveIssueGoalId, resolveNextIssueGoalId } from "./issue-goal-fallback.js";
import { getRunLogStore } from "./run-log-store.js";
import { getDefaultCompanyGoal } from "./goals.js";
import {
isVerifiedIssueTreeControlInteractionWake,
@@ -76,6 +77,10 @@ const ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE = 500;
export const MAX_CHILD_ISSUES_CREATED_BY_HELPER = 25;
const MAX_CHILD_COMPLETION_SUMMARIES = 20;
const CHILD_COMPLETION_SUMMARY_BODY_MAX_CHARS = 500;
const ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_LOG_BYTES = 2_000_000;
const ISSUE_COMMENT_RUN_LOG_DERIVATION_CHUNK_BYTES = 256_000;
const ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS = 60_000;
const ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_PARALLEL_READS = 8;
function assertTransition(from: string, to: string) {
if (from === to) return;
if (!ALL_ISSUE_STATUSES.includes(to)) {
@@ -118,6 +123,86 @@ function buildReusedExecutionWorkspaceConfigPatchFromIssueSettings(
};
}
function toTimestampMs(value: Date | string | null | undefined) {
if (!value) return null;
const date = value instanceof Date ? value : new Date(value);
const timestamp = date.getTime();
return Number.isFinite(timestamp) ? timestamp : null;
}
type IssueCommentRunLogAttributionCandidate = {
id: string;
createdAt: Date | string;
authorAgentId?: string | null;
authorUserId?: string | null;
createdByRunId?: string | null;
};
type IssueCommentRunLogAttributionRun = {
runId: string;
agentId: string;
createdAt: Date | string;
startedAt?: Date | string | null;
finishedAt?: Date | string | null;
logContent: string;
};
export function deriveIssueCommentRunLogAttribution(
comments: readonly IssueCommentRunLogAttributionCandidate[],
runs: readonly IssueCommentRunLogAttributionRun[],
) {
const derivedByCommentId = new Map<string, {
derivedAuthorAgentId: string;
derivedCreatedByRunId: string;
derivedAuthorSource: "run_log_comment_post";
}>();
for (const comment of comments) {
if (comment.authorAgentId || !comment.authorUserId || comment.createdByRunId) continue;
const commentCreatedAtMs = toTimestampMs(comment.createdAt);
if (commentCreatedAtMs === null) continue;
let bestMatch:
| {
runId: string;
agentId: string;
distanceMs: number;
}
| null = null;
for (const run of runs) {
const runStartMs = toTimestampMs(run.startedAt ?? run.createdAt);
const runEndMs = toTimestampMs(run.finishedAt ?? run.createdAt);
if (runStartMs === null || runEndMs === null) continue;
if (
commentCreatedAtMs < runStartMs
|| commentCreatedAtMs > runEndMs + ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS
) {
continue;
}
if (!run.logContent.includes(`comment id: ${comment.id}`)) continue;
const distanceMs = Math.abs(runEndMs - commentCreatedAtMs);
if (!bestMatch || distanceMs < bestMatch.distanceMs) {
bestMatch = {
runId: run.runId,
agentId: run.agentId,
distanceMs,
};
}
}
if (!bestMatch) continue;
derivedByCommentId.set(comment.id, {
derivedAuthorAgentId: bestMatch.agentId,
derivedCreatedByRunId: bestMatch.runId,
derivedAuthorSource: "run_log_comment_post",
});
}
return derivedByCommentId;
}
export interface IssueFilters {
status?: string;
assigneeAgentId?: string;
@@ -1779,6 +1864,124 @@ export function issueService(db: Db) {
};
}
async function readRunLogText(run: {
logStore: string | null;
logRef: string | null;
logBytes: number | null;
}) {
if (run.logStore !== "local_file" || !run.logRef) return "";
const logBytes = Number(run.logBytes ?? 0);
if (!Number.isFinite(logBytes) || logBytes <= 0) return "";
const store = getRunLogStore();
let offset = 0;
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;
}
return content;
}
async function enrichCommentsWithDerivedAgentAttribution<
T extends {
id: string;
companyId: string;
issueId: string;
authorAgentId?: string | null;
authorUserId?: string | null;
createdByRunId?: string | null;
createdAt: Date | string;
},
>(comments: readonly T[]) {
const candidates = comments.filter((comment) =>
!comment.authorAgentId
&& !!comment.authorUserId
&& !comment.createdByRunId,
);
if (candidates.length === 0) return comments;
const companyId = comments[0]?.companyId ?? null;
const issueId = comments[0]?.issueId ?? null;
if (!companyId || !issueId) return comments;
const minCommentCreatedAtMs = candidates.reduce<number | null>((min, comment) => {
const timestamp = toTimestampMs(comment.createdAt);
if (timestamp === null) return min;
return min === null ? timestamp : Math.min(min, timestamp);
}, null);
const maxCommentCreatedAtMs = candidates.reduce<number | null>((max, comment) => {
const timestamp = toTimestampMs(comment.createdAt);
if (timestamp === null) return max;
return max === null ? timestamp : Math.max(max, timestamp);
}, null);
if (minCommentCreatedAtMs === null || maxCommentCreatedAtMs === null) return comments;
const runs = await db
.select({
runId: heartbeatRuns.id,
agentId: heartbeatRuns.agentId,
createdAt: heartbeatRuns.createdAt,
startedAt: heartbeatRuns.startedAt,
finishedAt: heartbeatRuns.finishedAt,
logStore: heartbeatRuns.logStore,
logRef: heartbeatRuns.logRef,
logBytes: heartbeatRuns.logBytes,
})
.from(heartbeatRuns)
.where(
and(
eq(heartbeatRuns.companyId, companyId),
or(
sql`${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issueId}`,
sql`exists (
select 1
from ${activityLog}
where ${activityLog.companyId} = ${companyId}
and ${activityLog.entityType} = 'issue'
and ${activityLog.entityId} = ${issueId}
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)}`,
),
)
.orderBy(desc(heartbeatRuns.createdAt));
if (runs.length === 0) return comments;
const runsWithLogs: Array<(typeof runs)[number] & { logContent: string }> = [];
for (let index = 0; index < runs.length; index += ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_PARALLEL_READS) {
const batch = runs.slice(index, index + ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_PARALLEL_READS);
const batchWithLogs = await Promise.all(batch.map(async (run) => ({
...run,
logContent: await readRunLogText(run),
})));
runsWithLogs.push(...batchWithLogs);
}
const derivedByCommentId = deriveIssueCommentRunLogAttribution(candidates, runsWithLogs);
if (derivedByCommentId.size === 0) return comments;
return comments.map((comment) => {
const derived = derivedByCommentId.get(comment.id);
return derived ? { ...comment, ...derived } : comment;
});
}
async function assertAssignableAgent(companyId: string, agentId: string) {
const assignee = await db
.select({
@@ -3778,7 +3981,8 @@ export function issueService(db: Db) {
const comments = limit ? await query.limit(limit) : await query;
const { censorUsernameInLogs } = await instanceSettings.getGeneral();
return comments.map((comment) => redactIssueComment(comment, censorUsernameInLogs));
const enrichedComments = await enrichCommentsWithDerivedAgentAttribution(comments);
return enrichedComments.map((comment) => redactIssueComment(comment, censorUsernameInLogs));
},
getCommentCursor: async (issueId: string) => {
@@ -3809,16 +4013,17 @@ export function issueService(db: Db) {
};
},
getComment: (commentId: string) =>
instanceSettings.getGeneral().then(({ censorUsernameInLogs }) =>
db
getComment: async (commentId: string) => {
const { censorUsernameInLogs } = await instanceSettings.getGeneral();
const comment = await db
.select()
.from(issueComments)
.where(eq(issueComments.id, commentId))
.then((rows) => {
const comment = rows[0] ?? null;
return comment ? redactIssueComment(comment, censorUsernameInLogs) : null;
})),
.then((rows) => rows[0] ?? null);
if (!comment) return null;
const [enrichedComment] = await enrichCommentsWithDerivedAgentAttribution([comment]);
return redactIssueComment(enrichedComment ?? comment, censorUsernameInLogs);
},
removeComment: async (commentId: string) => {
const currentUserRedactionOptions = {
+66
View File
@@ -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 = [
+27 -8
View File
@@ -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",