mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
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:
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),
|
||||
|
||||
Reference in new issue
Block a user