Files
PaperClipAI/scripts/benchmark-agent-authentication.ts
T
Devin FoleyandPaperclip 4de6335c3b perf(server): trim saved run JSON from agent authentication (#16577)
## Thinking Path

> - Paperclip coordinates AI agents that work for companies.
> - Agent JWT authentication checks each request against its saved run.
> - Saved runs can contain large prompts and result payloads.
> - The first run lookup needs only identity fields, cancellation state,
and the conversation flag.
> - Reading all saved JSON transfers data that this lookup does not use.
> - This PR selects the required JSON fields in PostgreSQL and preserves
the existing authorization decisions.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The initial saved-run lookup in agent JWT authentication.

**Subsystem affected**

Server authentication and database queries.

**Current behavior**

The lookup transfers the full context and result JSON on each request.
It uses only the cancellation state and conversation flag from those
values.

**Proposed behavior**

Select the required JSON fields in PostgreSQL. Keep the run, company,
and agent predicates. Read current state on every request.

**Reason and benefit**

A synthetic lookup with a 96 KiB prompt and 4 KiB result returns 102,520
JSON bytes before this change and 134 bytes after it. This reduces data
transferred by this query.

**Breaking changes**

None intended. Identity fields, write revocation, read access, and
cancellation response codes retain their current behavior.

Related: #9280 addresses authentication round-trip counts. This PR only
reduces the data returned by the existing initial run lookup.

## What Changed

- Extract the initial run lookup and select only the JSON fields that
authentication consumes.
- Add real PostgreSQL tests for legacy JSON shapes, fresh cancellation
state, payload size, and run/company/agent scope.
- Add a reproducible benchmark that loads the original query from an
explicit baseline commit.
- Document the lookup and benchmark.

## Verification

- Baseline: `06689c1b3410000bcd65ff40cb469e600209c4db`.
- `pnpm -r typecheck`: passed.
- `pnpm build`: passed.
- Final server typecheck: passed.
- Final-head GitHub checks: all green at
`2be1c38e6bf51616b69993cf84cee1eef9657517`. Apex: 5/5, zero unresolved
threads, conflict-free.
- `pnpm check:module-boundaries`: passed. The standalone synthetic
benchmark now uses the existing root benchmark directory and resolves
Drizzle through the server workspace.
- Focused HTTP middleware and database tests: 29 passed on real Homebrew
PostgreSQL 16.14. The SQL test compares 270 combinations of status,
cancellation JSON, and conversation JSON. It also checks fresh state and
company/agent/run scope.
- Broad native verification: 873 passed across the HTTP middleware, SQL
projection, run diagnostics, and native executor suites.
- The default local full command completed its general server lane with
12,090 passed and 8,885 skipped tests. It could not finish the root
workflow: the host could not start the bundled PostgreSQL; a diagnostics
assertion matched a directory name; and Git fixtures retained the host
default branch. These failures remain in the verification record. All 68
failed files now pass on replay with disposable Homebrew PostgreSQL, an
isolated checkout, and command-scoped Git defaults. The 68-file replay
passed 2,588 tests and left two email skill-resolution failures. Moving
the checkout outside an unrelated ancestor skills directory resolved
those last failures: all 44 tests in that file passed. Product code and
test assertions were unchanged for these host adjustments.

Benchmark command:

```sh
pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-agent-authentication.ts --baseline-ref=06689c1b3410000bcd65ff40cb469e600209c4db
```

The final benchmark ran at treatment commit
`2be1c38e6bf51616b69993cf84cee1eef9657517` with a clean worktree. The
local run used PostgreSQL 16.14 and the maintained test constructor seam
to start disposable Homebrew clusters. The bundled PostgreSQL binary
could not start on this host. Both versions use the same schema and
predicates. Each case has two warmup rounds and ten measured rounds with
alternating order. Random content prevents unrealistic compression.
Complete authentication decisions match.

| Context / result | Before JSON bytes | After JSON bytes | Before
median | After median |
| --- | ---: | ---: | ---: | ---: |
| 1 KiB / 4 KiB | 5,240 | 134 | 0.157 ms | 0.157 ms |
| 96 KiB / 4 KiB | 102,520 | 134 | 0.331 ms | 0.140 ms |

These are local query round trips, including result transfer and
decoding. The small control has no material latency gain. The large case
improves by about 2.4x. Both plans read the same single row. The primary
result is reduced response size. This does not measure HTTP latency,
server CPU savings, or fleet-wide cost savings. Full samples and plans
are retained with the verification evidence.

## Risks

- JSON types must retain their meaning. The real database tests compare
the consumed authorization contract with the original projection,
including SQL null, JSON null, scalars, arrays, objects, string
booleans, and cancellation states.
- The change covers the first JWT run lookup. Later run identity capture
keeps its existing full-run reads and transaction locks. Those reads
remain a separate cost.
- The change adds no schema migration or authorization cache.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, code editing, and shell
tools. This session did not expose a more specific serving model ID or
its context window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-16 11:44:14 -07:00

86 lines
5.5 KiB
TypeScript

import assert from "node:assert/strict";
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
const { and, eq } = createRequire(new URL("../server/package.json", import.meta.url))("drizzle-orm");
import { agents, companies, createDb, heartbeatRuns, startEmbeddedPostgresTestDatabase, type Db } from "../packages/db/src/index.js";
import { agentRunWritesRevoked } from "../server/src/agent-run-cancellation.js";
import { loadAgentRunAuthentication } from "../server/src/middleware/agent-run-authentication.js";
type Query = { sql: string; params: unknown[] };
const baselineRef = process.argv.find(arg => arg.startsWith("--baseline-ref="))?.slice(15);
assert.ok(baselineRef, "Supply an explicit pre-fix --baseline-ref");
const treatmentRevision = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
assert.equal(execFileSync("git", ["status", "--porcelain"], { encoding: "utf8" }).trim(), "",
"Commit benchmark and treatment changes before measuring");
const baselineRevision = execFileSync("git", ["rev-parse", "--verify", `${baselineRef}^{commit}`], { encoding: "utf8" }).trim();
const source = execFileSync("git", ["show", `${baselineRevision}:server/src/middleware/auth.ts`], { encoding: "utf8" });
const expression = source.match(/const \[identityRun\] = await (db\.select\([\s\S]*?);\n if/)?.[1];
assert.ok(expression, "The baseline must contain the original JWT identity query");
const keepAlive = setInterval(() => {}, 1000);
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
let db: Db | undefined;
try {
database = await startEmbeddedPostgresTestDatabase("agent-authentication-benchmark-");
db = createDb(database.connectionString);
const companyId = randomUUID(), agentId = randomUUID(), runId = randomUUID();
await db.insert(companies).values({ id: companyId, name: "Synthetic authentication", issuePrefix: "AUTH" });
await db.insert(agents).values({ id: agentId, companyId, name: "Fixture", adapterType: "process" });
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, status: "running",
activeIdentityContextId: randomUUID(), responsibleUserId: "fixture-user" });
const baseline = new Function("db", "heartbeatRuns", "and", "eq", "claims", `return ${expression}`)(
db, heartbeatRuns, and, eq, { sub: agentId, company_id: companyId, run_id: runId },
).toSQL() as Query;
const treatment = loadAgentRunAuthentication(db, { companyId, agentId, runId }).toSQL();
function contract(rows: unknown[][]) {
return rows.map(([activeIdentityContextId, responsibleUserId, status, resultJson, contextSnapshot]) => ({
activeIdentityContextId, responsibleUserId, status,
writesRevoked: agentRunWritesRevoked({ status: status as string, resultJson: resultJson as Record<string, unknown> | null }),
conversation: (contextSnapshot as Record<string, unknown> | null)?.conversationMode === true,
}));
}
const results = [];
for (const contextBytes of [1024, 96 * 1024]) {
const context = { prompt: randomBytes(contextBytes * 3 / 4).toString("base64"), conversationMode: true };
const result = { summary: randomBytes(3 * 1024).toString("base64") };
await db.update(heartbeatRuns).set({ contextSnapshot: context, resultJson: result }).where(eq(heartbeatRuns.id, runId));
await db.$client.unsafe("ANALYZE heartbeat_runs");
const samples: Record<string, number[]> = { baseline: [], treatment: [] };
const rows: Record<string, unknown[][]> = {};
for (let iteration = 0; iteration < 12; iteration++) {
const order = iteration % 2 ? ["treatment", "baseline"] : ["baseline", "treatment"];
for (const name of order) {
const query = name === "baseline" ? baseline : treatment;
const start = performance.now();
rows[name] = await db.$client.unsafe(query.sql, query.params as never[]).values();
if (iteration >= 2) samples[name].push(performance.now() - start);
}
}
assert.deepEqual(contract(rows.treatment), contract(rows.baseline));
const measurements = [];
for (const [name, query] of Object.entries({ baseline, treatment })) {
const sorted = [...samples[name]].sort((a, b) => a - b);
const [plan] = await db.$client.unsafe(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query.sql}`, query.params as never[]);
measurements.push({ name, samplesMs: samples[name], medianMs: sorted[5],
returnedJsonBytes: Buffer.byteLength(JSON.stringify(rows[name])), plan: plan["QUERY PLAN"] });
}
results.push({ contextBytes: Buffer.byteLength(JSON.stringify(context)),
resultBytes: Buffer.byteLength(JSON.stringify(result)), contractEqual: true, measurements });
}
const [version] = await db.$client.unsafe("SELECT version() AS version");
const settings = await db.$client.unsafe("SELECT name, setting, unit FROM pg_settings WHERE name IN ('shared_buffers','work_mem','effective_cache_size','jit') ORDER BY name");
console.log(JSON.stringify({ baselineRevision, treatmentRevision, node: process.version,
dependencyLockfileSha256: createHash("sha256").update(readFileSync(new URL("../pnpm-lock.yaml", import.meta.url))).digest("hex"),
postgres: version.version, settings, fixtureRuns: 1, queries: { baseline, treatment }, results }, null, 2));
} finally {
try {
await db?.$client.end({ timeout: 0 });
await database?.cleanup();
} finally {
clearInterval(keepAlive);
}
}