Files
PaperClipAI/server/src/services/native-runtime/external-review-wait.ts
T
Devin FoleyandPaperclip bf142db65a perf(server): narrow external review wait context reads (#17562)
Project current chat authorization fields for external review-wait discovery instead of saved execution input. Preserve typed source bindings, nested question receipts, candidate eligibility, live revocation checks, and stored context.

Validation: 181 native chat integration tests, full build and typecheck, exact-head Greptile 5/5 (medium), full green CI, and no unresolved review threads. The full-context counterfactual fails both direct-chat size regressions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 00:02:32 -07:00

73 lines
3.5 KiB
TypeScript

import { and, eq, inArray, sql } from "drizzle-orm";
import { chatConversations, chatEndpoints, heartbeatRuns, issues, statusDecisions, type Db } from "@paperclipai/db";
import { authorizeChatConversationForBoundRun } from "./chat-attachment-reuse.js";
import { EXTERNAL_CHAT_QUESTION_RESPONSE_KEY } from "./external-chat-question-response.js";
const authorizationContextKeys = [
"source", "issueId", "wakeReason", "interactionKind", "interactionStatus", "externalChatContinuation",
"interactionId", "sourceRunId", "sourceCommentId", "wakeCommentId", "wakeCommentIds",
"paperclipHarnessCheckedOut", "paperclipExternalChatExecutionBound",
EXTERNAL_CHAT_QUESTION_RESPONSE_KEY,
] as const;
const authorizationContext = sql<Record<string, unknown> | null>`
case when ${heartbeatRuns.contextSnapshot} is null then null else (
select coalesce(jsonb_object_agg(field.key, field.value), '{}'::jsonb)
from jsonb_each(case when jsonb_typeof(${heartbeatRuns.contextSnapshot}) = 'object'
then ${heartbeatRuns.contextSnapshot} else '{}'::jsonb end) as field(key, value)
where field.key in (${sql.join(authorizationContextKeys.map(key => sql`${key}`), sql`, `)})
) end
`;
export async function listExternalReviewWaits(db: Db, companyId: string, issueIds: string[]) {
if (!issueIds.length) return [];
const candidates = await db.selectDistinctOn([issues.id], {
id: statusDecisions.id,
companyId: issues.companyId,
issueId: issues.id,
status: chatConversations.state,
createdAt: statusDecisions.createdAt,
runId: heartbeatRuns.id,
agentId: heartbeatRuns.agentId,
contextSnapshot: authorizationContext,
}).from(issues)
.innerJoin(statusDecisions, and(
eq(statusDecisions.id, issues.lastStatusDecisionId),
eq(statusDecisions.companyId, issues.companyId),
eq(statusDecisions.issueId, issues.id),
eq(statusDecisions.applicationState, "applied"),
eq(statusDecisions.toStatus, "in_review"),
eq(statusDecisions.reasonCode, "external_chat_response_waiting"),
))
.innerJoin(heartbeatRuns, and(
eq(heartbeatRuns.id, statusDecisions.runId),
eq(heartbeatRuns.companyId, issues.companyId),
eq(heartbeatRuns.nativeIssueId, issues.id),
eq(heartbeatRuns.agentId, issues.assigneeAgentId),
eq(heartbeatRuns.status, "succeeded"),
))
.innerJoin(chatConversations, and(
eq(chatConversations.companyId, issues.companyId),
eq(chatConversations.issueId, issues.id),
inArray(chatConversations.state, ["active", "waiting"]),
))
.innerJoin(chatEndpoints, and(
eq(chatEndpoints.id, chatConversations.endpointId),
eq(chatEndpoints.companyId, issues.companyId),
eq(chatEndpoints.assignedAgentId, issues.assigneeAgentId),
eq(chatEndpoints.status, "active"),
))
.where(and(eq(issues.companyId, companyId), inArray(issues.id, issueIds), eq(issues.status, "in_review")));
const waits = await Promise.all(candidates.map(async ({ runId, agentId, contextSnapshot, ...wait }) => {
try {
await authorizeChatConversationForBoundRun(db, { companyId, issueId: wait.issueId, runId, agentId }, contextSnapshot, "read");
return wait;
} catch (error) {
if (error instanceof Error && ["paperclip_runner_chat_attachment_binding_denied",
"paperclip_runner_chat_attachment_destination_denied", "paperclip_runner_chat_attachment_principal_denied"].includes(error.message)) return null;
throw error;
}
}));
return waits.filter((wait): wait is NonNullable<typeof wait> => wait !== null);
}