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

@@ -492,6 +492,9 @@ binding, and chat source fields under the existing nonblocking share lock.
It excludes saved execution input, runner profiles, and log excerpts. Actual
retry admission still reads the complete run and its recovery evidence. These
reads preserve stored run data, source revocation checks, and JSON value types.
External review-wait discovery selects only chat authorization context fields.
It keeps the current endpoint, membership, resource, and question-receipt checks;
saved prompts remain stored and do not cross this discovery query.
Legacy continuation recovery rejects native runs before it loads their saved
execution input. The persisted disposition check reads only the task binding
@@ -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),