Files
PaperClipAI/scripts/benchmark-native-session-resume-reads.ts
T
DottaandPaperclip dc8cbdb819 perf(server): narrow native session resume database reads (#17934)
<!-- entire-trail-link-start -->
https://entire.com/gh/paperclipai/-/trails/94/changes/paperclip/94
<!-- entire-trail-link-end -->

## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native agents resume prior provider sessions from saved run
checkpoints.
> - Resume selection must respect every newer progress barrier.
> - The prior query returned large context and profile JSON for up to 32
candidates.
> - Most candidates need only process, status, binding, and
checkpoint-presence checks.
> - This PR selects the same source in one SQL snapshot and returns only
its required payloads.
> - This reduces database response bytes while preserving session
authority and saved task context.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Native task/session resume selection in `findNativeSessionResumeRun`.

**Current behavior**

At baseline `bf3ef12f3a78d0b15a32ad57f7e16520fbf6cd5e`, a resume lookup
reads complete context and profile JSON for up to 32 runs, then reads
provider-event evidence. A synthetic 15-run history returns about 4.44
MB even though one checkpoint is selected.

**Proposed behavior**

A bounded CTE finds the first checkpoint or progress barrier. The same
SQL statement returns the selected source's native input, checkpoint,
tool fingerprint, wake envelope, canonical task brief, and process
identity. All fallback barriers and the 32-run limit stay in place.

**Reason and benefit**

Rejected candidates no longer transfer saved prompts and execution
inputs to Node. A single snapshot also prevents a gap between candidate
selection and payload hydration.

**Breaking changes**

None to public APIs or stored history. This private service returns the
fields consumed by runtime selection and checkpoint rebinding. It keeps
complete required nested values and distinguishes missing brief fields
from explicit JSON nulls.

**Additional context**

Checked `ROADMAP.md`. This is a targeted improvement to existing resume
behavior. Public searches for native session resume projections found no
duplicate. Related work includes #6573 (durable agent sessions) and
#17011 (managed Claude resume opt-in); neither changes this database
read. Provider-refusal recovery in #17676 is a separate concern and is
unchanged.

## What Changed

- Select the newest progress barrier within the existing exact
company/agent/task/session and 32-run window in one SQL snapshot.
- Return three required profile fields, the full wake envelope, and the
saved task's ID, title, and description only for the selected source.
- Preserve invalid non-null checkpoint barriers, provider evidence,
process authority, terminal-status rules, timestamp precision, and exact
string binding checks.
- Add real PostgreSQL regression coverage and a reproducible
baseline/treatment benchmark. Document the read and benchmark command.

## Verification

- `pnpm -r typecheck` passed. `pnpm build` passed. The final server `tsc
--noEmit` also passed. The later benchmark-only relocation passed `pnpm
check:module-boundaries` and the real PostgreSQL proof; runtime and
regression-test source are unchanged.
- Related suites: `native-session-resume-projection.test.ts`,
`native-session-resume.test.ts`, `heartbeat/retries.test.ts`, and
`heartbeat-retry-scheduling.test.ts`: 78 passed and 54 skipped across 3
passed files and 1 skipped file. Embedded PostgreSQL/platform gates
caused the skips. Resume orchestration and the 10 new SQL tests ran
against isolated PostgreSQL 17.11. The final 10-test projection rerun
passed.
- New SQL cases cover all seven provider evidence kinds, each process
authority field, all terminal statuses, wrong bindings and runtime,
invalid checkpoints, null/scalar/array/object JSON, missing versus null
brief fields, the 32-row bound, ordering ties, future barriers,
submillisecond timestamps, and a change committed before the one
selection statement.
- `pnpm test:run` was attempted. The host's embedded PostgreSQL startup
failed after five attempts. The broad run recorded failures/skips in
embedded-database suites and was stopped after that concrete host
limitation (exit 143). It is not a local full-suite pass. The full
final-head CI matrix passes on
`dcde4e3dac2173321d7be2406e7775af8411826a`: 55 successful checks, one
neutral check, and two optional Storybook skips. Greptile completed at
5/5 on that exact head, with zero unresolved findings. The initial
benchmark-location policy failure was fixed before this final run.
- Reproduce the benchmark with `pnpm --filter @paperclipai/server exec
tsx ../scripts/benchmark-native-session-resume-reads.ts
--baseline-ref=bf3ef12f3a78d0b15a32ad57f7e16520fbf6cd5e`. The optional
`PAPERCLIP_SESSION_RESUME_BENCHMARK_DATABASE_URL` must name an isolated
database ending in `_test`.
- The harness imports the pinned baseline module, invokes both actual
services, captures complete generated SQL and parameters, compares every
retained consumer field, and captures `EXPLAIN (ANALYZE, BUFFERS, FORMAT
JSON)`. Its incompressible wide fixture keeps a complete 96 KiB native
input in the selected result. Raw PostgreSQL text DataRow frames are
measured exactly; RowDescription, protocol command metadata, TLS, and
TCP are excluded.

| Synthetic local case | Baseline DataRow bytes | Treatment DataRow
bytes | Reduction |
| --- | ---: | ---: | ---: |
| 15 runs, one selected checkpoint | 4,441,143 | 104,599 | 97.64% |
| 15 unused bootstraps | 4,435,590 | 0 | 100% |
| One small selected source | 6,395 | 6,319 | 1.19% |

The committed-head benchmark uses two warmups and seven alternating
samples per variant. Local round-trip medians and ranges were:

| Synthetic local case | Baseline median (range), ms | Treatment median
(range), ms |
| --- | ---: | ---: |
| Wide history | 20.32 (18.63–24.37) | 1.63 (1.40–2.21) |
| Unused history | 18.52 (17.63–20.35) | 1.20 (1.13–1.56) |
| Small selected source | 1.05 (0.95–1.70) | 0.78 (0.73–1.30) |

The small case has overlapping ranges and a 1.19% byte change, so it is
neutral. `EXPLAIN` server execution time was higher for the treatment
(wide: 0.402 ms versus 0.028 ms across the two baseline statements).
`EXPLAIN` does not serialize result rows; the new JSON presence checks
do extra server work while avoiding large result transfer. These are
local synthetic round-trip measurements, not production latency claims.
The final proof ran on `dcde4e3dac2173321d7be2406e7775af8411826a` with
no runtime source diff.

## Risks

- Selection policy now has a SQL form. Regression tests preserve the
pure selector's barrier semantics, including invalid non-null
checkpoints and JavaScript Date precision.
- Checkpoint-presence checks can still detoast candidate profiles inside
PostgreSQL. The change reduces returned bytes; it does not eliminate all
database JSON work.
- The selected checkpoint, native input, and wake envelope can still be
large because consumers require them in full. No new payload cap is
introduced.
- There is no migration, cache, background polling, ownership lock
change, deployment, or merge in this PR.

## Model Used

OpenAI GPT-6 through Codex. The host did not expose an exact runtime
model ID or context-window size. The agent used reasoning, code
execution, real PostgreSQL, and GitHub tools.

## 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-27 16:31:55 -05:00

128 lines
9.0 KiB
TypeScript

import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { randomBytes, randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { agents, applyPendingMigrations, companies, createDb, heartbeatRuns, issues, startEmbeddedPostgresTestDatabase, type Db } from "../packages/db/src/index.js";
import { findNativeSessionResumeRun } from "../server/src/services/native-runtime/native-session-resume.js";
import { resolveExternalTestDatabaseUrl } from "../server/src/__tests__/helpers/external-test-database.js";
type Query = { sql: string; params: unknown[] };
const root = fileURLToPath(new URL("../", import.meta.url));
const baselineRef = process.argv.find(arg => arg.startsWith("--baseline-ref="))?.slice(15);
assert.ok(baselineRef, "Pass a pinned pre-fix --baseline-ref");
const baselineRevision = execFileSync("git", ["rev-parse", "--verify", `${baselineRef}^{commit}`], { cwd: root, encoding: "utf8" }).trim();
const sourcePath = "server/src/services/native-runtime/native-session-resume.ts";
const treatmentRevision = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
const treatmentDiff = execFileSync("git", ["diff", "HEAD", "--", sourcePath], { cwd: root, encoding: "utf8" });
const externalUrl = resolveExternalTestDatabaseUrl("PAPERCLIP_SESSION_RESUME_BENCHMARK_DATABASE_URL");
const database = externalUrl ? { connectionString: externalUrl, cleanup: async () => {} } : await startEmbeddedPostgresTestDatabase("native-session-resume-benchmark-");
const db = createDb(database.connectionString);
const baselineDir = await fs.mkdtemp(path.join(root, "server/scripts/.session-resume-baseline-"));
const companyIds: string[] = [];
try {
if (externalUrl) await applyPendingMigrations(externalUrl);
const moduleUrl = new URL(`../${sourcePath}`, import.meta.url);
const source = execFileSync("git", ["show", `${baselineRevision}:${sourcePath}`], { cwd: root, encoding: "utf8" });
await fs.writeFile(path.join(baselineDir, "resume.ts"), source.replace(/from "(\.{1,2}\/[^\"]+)"/g,
(_match, specifier: string) => `from ${JSON.stringify(fileURLToPath(new URL(specifier, moduleUrl)))}`));
const baseline = await import(pathToFileURL(path.join(baselineDir, "resume.ts")).href) as { findNativeSessionResumeRun: typeof findNativeSessionResumeRun };
async function capture(invoke: typeof findNativeSessionResumeRun, input: Parameters<typeof findNativeSessionResumeRun>[1]) {
const queries: Query[] = [];
const wrap = (target: Db): Db => new Proxy(target, { get(target, key) {
if (key === "with") return (...args: Parameters<Db["with"]>) => wrap(target.with(...args) as Db);
if (key !== "select") return Reflect.get(target, key);
return (...fields: Parameters<Db["select"]>) => {
const builder = target.select(...fields), from = builder.from.bind(builder);
builder.from = ((...args: Parameters<typeof from>) => {
const query = from(...args), execute = query.execute.bind(query);
query.execute = async (...executeArgs: Parameters<typeof execute>) => {
queries.push(query.toSQL());
return execute(...executeArgs);
};
return query;
}) as typeof builder.from;
return builder;
};
}});
return { result: await invoke(wrap(db), input), queries };
}
const median = (values: number[]) => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)];
const measurements = [];
for (const scenario of ["wide_history", "unused_history", "small_selected"] as const) {
const companyId = randomUUID(), agentId = randomUUID(), issueId = randomUUID(), normalizedSessionId = randomUUID();
companyIds.push(companyId);
await db.insert(companies).values({ id: companyId, name: "Synthetic resume benchmark", issuePrefix: `B${companyId.replaceAll("-", "")}` });
await db.insert(agents).values({ id: agentId, companyId, name: "Synthetic runner", adapterType: "paperclip_runner" });
await db.insert(issues).values({ id: issueId, companyId, title: "Retain canonical task" });
const runCount = scenario === "small_selected" ? 1 : 15;
const prompt = randomBytes(scenario === "small_selected" ? 16 : 144 * 1024).toString("base64");
const profilePadding = randomBytes(scenario === "small_selected" ? 16 : 72 * 1024).toString("base64");
const neededInput = { task: { title: "Prior task", description: "Canonical description", prompt: profilePadding }, runtimeContext: { instructions: "Keep complete input" } };
const checkpoint = { sessionId: normalizedSessionId, state: randomBytes(4096).toString("base64") };
await db.insert(heartbeatRuns).values(Array.from({ length: runCount }, (_, n) => ({
id: randomUUID(), companyId, agentId, nativeIssueId: issueId, nativeSessionId: normalizedSessionId,
runtimeMode: "native", status: "failed", createdAt: new Date(Date.UTC(2026, 8, 8, 0, 0, n)),
runnerProfileJson: { padding: "Unused profile metadata", nativeExecutionInput: neededInput, nativeToolContractFingerprint: "fixture",
...(scenario !== "unused_history" && n === 0 ? { sessionCheckpoint: checkpoint } : {}) },
contextSnapshot: { prompt, paperclipWake: { externalChatProvider: "slack", githubReviewConfigurations: [{ key: "review" }] },
paperclipIssue: { id: issueId, title: "Prior canonical title", description: "Prior canonical description" } },
})));
await db.$client.unsafe("ANALYZE heartbeat_runs");
const input = { companyId, agentId, issueId, normalizedSessionId, currentRunId: randomUUID(), beforeCreatedAt: new Date("2026-09-09T00:00:00Z") };
const before = await capture(baseline.findNativeSessionResumeRun, input), after = await capture(findNativeSessionResumeRun, input);
const contract = (run: typeof after.result) => run && ({ ...run,
runnerProfileJson: { nativeExecutionInput: run.runnerProfileJson?.nativeExecutionInput ?? null,
nativeToolContractFingerprint: run.runnerProfileJson?.nativeToolContractFingerprint ?? null,
sessionCheckpoint: run.runnerProfileJson?.sessionCheckpoint ?? null },
contextSnapshot: { paperclipWake: run.contextSnapshot?.paperclipWake ?? null, paperclipIssue: run.contextSnapshot?.paperclipIssue ?? null } });
assert.deepEqual(contract(after.result), contract(before.result));
const variants = [before.queries, after.queries], samples: number[][] = [[], []], dataRowBytes = [0, 0], resultRows = [0, 0];
for (let iteration = 0; iteration < 9; iteration++) {
for (const variant of iteration % 2 ? [1, 0] : [0, 1]) {
const start = performance.now();
let bytes = 0, count = 0;
for (const query of variants[variant]) {
const rows = await db.$client.unsafe(query.sql, query.params as never[]).raw();
count += rows.length;
bytes += rows.reduce((total, row) => total + 7 + row.reduce((size: number, value: Buffer | null) => size + 4 + (value?.length ?? 0), 0), 0);
}
if (iteration >= 2) samples[variant].push(performance.now() - start);
dataRowBytes[variant] = bytes; resultRows[variant] = count;
}
}
const plans = [];
for (const queries of variants) {
const variantPlans = [];
for (const query of queries) {
const [plan] = await db.$client.unsafe(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query.sql}`, query.params as never[]);
variantPlans.push(plan["QUERY PLAN"]);
}
plans.push(variantPlans);
}
if (!process.argv.includes("--baseline-only") && scenario !== "small_selected") assert.ok(dataRowBytes[1] < dataRowBytes[0] / 10);
measurements.push({ scenario, runCount, equalConsumerFields: true, baselineQueryCount: before.queries.length, treatmentQueryCount: after.queries.length,
dataRowBytes, resultRows, baselineMedianMs: median(samples[0]), treatmentMedianMs: median(samples[1]), samples, queries: variants, plans });
}
console.log(JSON.stringify({ baselineRevision, treatmentRevision, treatmentDiff,
acceptance: "Equal selected consumer fields; wide and unused histories remove at least 90% of DataRow bytes; no timing threshold",
byteBoundary: "Exact text-format PostgreSQL DataRow frames (7 bytes framing + 4 bytes length per column + raw field bytes); excludes RowDescription, command metadata, TLS and TCP",
postgres: await db.$client.unsafe("select version()"), measurements }, null, 2));
} finally {
try {
for (const companyId of companyIds) {
await db.$client.unsafe("DELETE FROM heartbeat_runs WHERE company_id = $1::uuid", [companyId]);
await db.$client.unsafe("DELETE FROM issues WHERE company_id = $1::uuid", [companyId]);
await db.$client.unsafe("DELETE FROM agents WHERE company_id = $1::uuid", [companyId]);
await db.$client.unsafe("DELETE FROM companies WHERE id = $1::uuid", [companyId]);
}
} finally {
try { await db.$client.end({ timeout: 0 }); }
finally {
try { await database.cleanup(); }
finally { await fs.rm(baselineDir, { recursive: true, force: true }); }
}
}
}