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>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-10-25 00:02:32 -07:00
1 parent 08dafde1d6
commit bf142db65a
3 files changed
+68 -2

No files matched your search

@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { and, eq, ne, sql } from "drizzle-orm";
import { createDatabaseResponseCapture, installDatabaseResponseCapture } from "@paperclipai/db/response-capture";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import {
agentWakeupRequests,
@@ -71,6 +72,7 @@ import { voiceCredentialFingerprint } from "../voice/voice-session-store.js";
import { nativeSha256 } from "./canonical.js";
import { questionResponseDeliveryValues } from "../question-response-delivery.js";
import { resolveExternalChatQuestionResponse } from "./external-chat-question-response.js";
import { listExternalReviewWaits } from "./external-review-wait.js";
import { materializeExternalChatQuestionResponseInput } from "./external-chat-question-response-input.js";
import * as nativeInteractionBridge from "./native-interaction-bridge.js";
import { defaultGitHubReviewPolicy, type AskUserQuestionsInteraction } from "@paperclipai/shared";
@@ -866,6 +868,50 @@ describe("native external-chat response wait", () => {
};
}
it.each(["telegram", "github", "github_board_answer", "slack_board_answer"] as const)(
"reads small current external review waits without saved prompts (%s)", async source => {
const fixture = await (async () => {
if (source === "github_board_answer" || source === "slack_board_answer") {
const answered = await seedBoardAnsweredTurn(true, source === "github_board_answer" ? "github" : "slack");
await attestAnswer(answered);
return answered;
}
return seedWaitTurn(source);
})();
const finalized = await finalizeNativeRun({ db, runId: fixture.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
expect(finalized.phase).toBe("committed");
await db.update(statusDecisions).set({ toStatus: "in_review" }).where(eq(statusDecisions.id, finalized.decisionId!));
await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, fixture.issueId));
const [decision] = await db.select({ reason: statusDecisions.reasonCode, state: statusDecisions.applicationState })
.from(statusDecisions).where(eq(statusDecisions.id, finalized.decisionId!));
const governed = source === "github_board_answer" || source === "slack_board_answer";
expect(decision).toEqual({ reason: governed ? "governed_response_waiting" : "external_chat_response_waiting", state: "applied" });
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId));
expect(run!.status).toBe("succeeded");
const contextSnapshot = { ...run!.contextSnapshot, savedPrompt: "external-wait-saved-input🙂".repeat(30_000) };
await db.update(heartbeatRuns).set({ contextSnapshot }).where(eq(heartbeatRuns.id, fixture.runId));
await authorizeChatConversationForBoundRun(db, fixture, contextSnapshot, "read");
const observed = createDb(temporary?.connectionString ?? externalTestDatabaseUrl!);
const events: Array<Record<string, unknown>> = [], now = Date.now();
const capture = createDatabaseResponseCapture({ start: now - 100, end: now + 59_000 }, event => events.push(event));
installDatabaseResponseCapture(observed, { issues, heartbeatRuns }, capture);
try {
expect(await listExternalReviewWaits(observed, fixture.companyId, [fixture.issueId])).toEqual(governed ? [] : [
expect.objectContaining({ companyId: fixture.companyId, issueId: fixture.issueId }),
]);
capture.flush();
const reads = events.filter(event => event.table === "issues" && event.operation === "select");
expect(reads.length).toBeGreaterThan(0);
expect(reads.reduce((bytes, event) => bytes + Number(event.responseValueBytes), 0)).toBeLessThan(4096);
expect(await listExternalReviewWaits(observed, randomUUID(), [fixture.issueId])).toEqual([]);
expect(await listExternalReviewWaits(observed, fixture.companyId, [])).toEqual([]);
await db.update(companyMemberships).set({ status: "suspended" }).where(eq(companyMemberships.companyId, fixture.companyId));
expect(await listExternalReviewWaits(observed, fixture.companyId, [fixture.issueId])).toEqual([]);
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, fixture.runId)))[0]?.contextSnapshot).toEqual(contextSnapshot);
} finally { capture.flush(); await observed.$client.end(); }
},
);
it("resumes an oversized Slack multi-select answered through its Paperclip handoff", async () => {
const fixture = await seedBoardAnsweredTurn(true, "slack", true);
const [publication] = await db.select().from(chatPublications).where(eq(chatPublications.id, fixture.publicationId));
@@ -1,6 +1,23 @@
import { and, eq, inArray } from "drizzle-orm";
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 [];
@@ -12,7 +29,7 @@ export async function listExternalReviewWaits(db: Db, companyId: string, issueId
createdAt: statusDecisions.createdAt,
runId: heartbeatRuns.id,
agentId: heartbeatRuns.agentId,
contextSnapshot: heartbeatRuns.contextSnapshot,
contextSnapshot: authorizationContext,
}).from(issues)
.innerJoin(statusDecisions, and(
eq(statusDecisions.id, issues.lastStatusDecisionId),