Files
PaperClipAI/scripts/benchmark-inbound-queue-discovery.ts
T
DottaandPaperclip a91f78e7e0 perf(chat): reduce inbound queue discovery cost (#17971)
## Thinking Path

> - Paperclip helps people manage companies of AI agents.
> - External chat messages enter a durable delivery queue.
> - Queue reads must preserve pending wake ownership and conversation
order.
> - Retained processed history increases the estimated cost of a
correlated pending-action test.
> - PostgreSQL 17 enables JIT for these small selectors and reads extra
JSON in deadline discovery.
> - This pull request uses a scoped pending-action set for open-work
discovery.
> - The benefit is less database work with the same selected deliveries
and deadlines.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Inbound delivery deadline discovery and conversation-head selection.

**Subsystem affected**

Server chat queue discovery.

**Current behavior**

A correlated `EXISTS` inside an `OR` has a high cost estimate on
retained completed history. PostgreSQL 17 already hashes its
pending-action read, but the estimate still enables JIT. Deadline
discovery also reads JSON from completed deliveries that have no pending
wake.

**Proposed behavior**

Use `(company_id, endpoint_id, delivery_id)` membership in the
pending-action set for the two open-work selectors. Keep ready-only
evaluation on its original scoped `EXISTS`. Keep retry, lease, ordering,
lifecycle and endpoint rules.

**Reason and benefit**

The local retained-history fixture reduces deadline selection time. The
small control and the final head selection are neutral. Ready-only
selection remains an unchanged control.

**Breaking changes**

None. No API, schema, migration, stored history, worker, timer or
notification changes.

Related work: #17647 and #15927 change inbound wake-notice scheduling
and selection. This PR changes the separate conversation queue ownership
predicate.

## What Changed

- Use a fully scoped pending-action set for open-work discovery. Exclude
null delivery IDs to preserve exact boolean results. Keep duplicate
actions from multiplying deliveries.
- Preserve the original ready-only query.
- Add five real PostgreSQL tests and a reproducible baseline/treatment
benchmark. Capture the complete real service SQL, parameters, positional
rows and execution plans.
- Document the query cause, rejected hypotheses, test results and limits
in `doc/plans/2026-10-27-inbound-queue-query-proof.md`.

## Verification

Pinned pre-fix baseline: `09ad9adce2cf4742944353a3707a2f908e9bb35e`.

Local fixture: Linux ARM64 PostgreSQL 17.11, Node 26.4.0. It has 1500
completed deliveries and actions, two open deliveries, and 32 KiB of
synthetic incompressible JSON per historical delivery. A separate
control has 20 completed rows. The normal settings use JIT on with a
100000 cost threshold, 128 MiB shared buffers and 4 MiB work_mem.

The final quiet retained-history pair measured these local SQL medians.
Each
arm has two warmups and five alternating samples. The small control is
neutral.

| Actual selector | Baseline | Treatment | Complete positional output |
| --- | ---: | ---: | --- |
| Next inbound deadline | 18.78 ms | 1.58 ms | Equal |
| Conversation head | 5.71 ms | 6.18 ms | Equal |
| Ordinary ready control | 9.23 ms | 8.33 ms | Equal ; unchanged |

The deadline plan uses fewer shared buffers. Head physical reads remain
unchanged. Deadline discovery avoids JIT on this fixture. The pristine
head estimate is below the JIT threshold in both arms, and its timing is
neutral. With JIT disabled, deadline discovery still improves; head and
ready timings overlap.

- Red: the unchanged baseline fails the explicit local fixture work
budget for JIT and deadline buffer reads. Green: the treatment passes.
There is no timing threshold in ordinary CI.
- Five new PostgreSQL tests and six existing assembled chat tests pass.
They cover scoped/null/duplicate pending actions, boolean results,
payloads, head barriers, reactions, paused/attention/AgentMail
exclusions, retry/processing deadlines, provider ordering, lease loss,
lifecycle retry and edit/admission races.
- Actual old/new ready selectors have equal error outcomes for unrelated
malformed retry timestamps. An initial AND-only oracle comparison was
inconclusive and is not defect evidence.
- `pnpm -r typecheck` passes.
- `pnpm build` passes.
- Final head `f3cac298d8a8a5377127b0db7057c52019bbe426` passes the
complete [CI
workflow](https://github.com/paperclipai/paperclip/actions/runs/40625611887).
Greptile is 5/5 on that head with no unresolved review threads. The
earlier end-to-end cleanup lock-timeout failure passed on this revision.
- The complete local suite was not run. The host's known native
PostgreSQL startup failure exhausts SysV semaphores. Focused integration
tests use a disposable Docker PostgreSQL constructor through the
existing test seam. Complete final-head CI is required.

Reproduce the real SQL comparison:

```sh
pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-inbound-queue-discovery.ts --baseline-ref=09ad9adce2cf4742944353a3707a2f908e9bb35e --require-work-budget --retained-only
```

Use `--jit-off` for the diagnostic control. The default command uses the
repository's bundled PostgreSQL version and records it in output. The
reported 17.11 run used the existing test constructor seam with a
disposable `postgres:17` container. Raw samples, complete queries,
parameters and plans are emitted as JSON. The benchmark uses disposable
synthetic data and does not read a live database URL.

## Risks

- Query plans depend on data and PostgreSQL settings. These local
measurements do not prove deployed speed or request latency.
- Head selection still reads historical JSON. Action history remains
stored and scanned. This is not a constant-time queue implementation.
- The changed false branch has no retry timestamp cast. Ready-only
parsing, claims and retries stay on their existing query.
- Background SQL totals can overlap. Do not add these savings into an
HTTP latency claim. The largest possible saving for an affected
selection is bounded by its observed SQL time, about 49–53 ms on average
in discovery.

## Model Used

OpenAI Codex, GPT-6 family, with tool use and code execution. The
runtime does not expose the exact model ID or context-window size. Three
parallel agents own separate performance fixes; this PR contains only
the inbound queue fix.

## 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-28 07:19:22 -05:00

135 lines
10 KiB
TypeScript

import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createDb, startEmbeddedPostgresTestDatabase, type Db } from "../packages/db/src/index.js";
import { captureInboundQuery, runQuery, seedInboundDiscovery, type QueueFactory, type ServiceFactory } from "../server/src/__tests__/helpers/inbound-discovery-fixture.js";
const ref = process.argv.find(arg => arg.startsWith("--baseline-ref="))?.slice(15);
assert.ok(ref, "Pass an explicit pre-change --baseline-ref=<commit>");
const baselineRevision = execFileSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], { encoding: "utf8" }).trim();
const root = fileURLToPath(new URL("../", import.meta.url));
const sources = ["server/src/services/chat-channels/inbound/delivery-ledger.ts", "server/src/services/chat-channels/inbound/conversation-queue.ts", "server/src/services/chat-channels.ts"];
const runtimeChanges = execFileSync("git", ["diff", "--name-only", baselineRevision, "--", "server/src/services", "packages/db/src", "packages/shared/src", "pnpm-lock.yaml"], { encoding: "utf8" }).trim().split("\n").filter(Boolean);
assert.deepEqual(runtimeChanges.filter(name => !sources.includes(name)), [], "Baseline must not import changed runtime helpers or dependencies");
const dir = await fs.mkdtemp(path.join(root, "server/scripts/.inbound-baseline-"));
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
let db: Db | undefined;
const keepAlive = setInterval(() => {}, 1000);
function nodes(plan: Record<string, unknown>): Record<string, unknown>[] {
return [plan, ...((plan.Plans ?? []) as Record<string, unknown>[]).flatMap(nodes)];
}
const started = performance.now();
try {
const targets = new Map(sources.map(source => [path.join(root, source).replace(/\.ts$/, ".js"), path.join(dir, path.basename(source))]));
for (const source of sources) {
const original = path.join(root, source);
const text = execFileSync("git", ["show", `${baselineRevision}:${source}`], { encoding: "utf8" });
const rewritten = text.replace(/from "(\.{1,2}\/[^\"]+)"/g, (_match, specifier: string) => {
const resolved = path.resolve(path.dirname(original), specifier);
return `from ${JSON.stringify(targets.get(resolved) ?? resolved)}`;
});
await fs.writeFile(path.join(dir, path.basename(source)), rewritten);
}
const oldQueue = await import(pathToFileURL(path.join(dir, "conversation-queue.ts")).href) as { createChatConversationDeliveryQueue: QueueFactory };
const oldService = await import(pathToFileURL(path.join(dir, "chat-channels.ts")).href) as { chatChannelService: ServiceFactory };
database = await startEmbeddedPostgresTestDatabase("inbound-discovery-benchmark-");
db = createDb(database.connectionString, { maxConnections: 1, idleTimeoutSeconds: 0 });
const [{ version }] = await db.$client.unsafe("select version()");
const settings = await db.$client.unsafe("select name, setting from pg_settings where name in ('shared_buffers', 'work_mem', 'jit', 'jit_above_cost', 'jit_inline_above_cost', 'random_page_cost', 'max_connections') order by name");
const results = [];
const malformedReadyControls = [];
let baselineExceedsWorkBudget = false;
const budgetFailures: string[] = [];
for (const fixture of [{ name: "small", history: 20, payloadBytes: 0 }, { name: "retained-history", history: 1500, payloadBytes: 32 * 1024 }].filter(value => !process.argv.includes("--retained-only") || value.name === "retained-history")) {
const f = await seedInboundDiscovery(db, fixture.history, fixture.payloadBytes);
await db.$client.unsafe("begin");
if (process.argv.includes("--jit-off")) await db.$client.unsafe("set local jit = off");
try {
const pairs = [];
for (const kind of ["deadline", "head", "ready"] as const) {
const before = await captureInboundQuery(db, f, kind, oldQueue.createChatConversationDeliveryQueue, oldService.chatChannelService);
const after = await captureInboundQuery(db, f, kind);
pairs.push({ kind, before, after });
}
const connection = await db.$client.reserve();
const timedDb = new Proxy(db, { get(target, key) { return key === "$client" ? connection : Reflect.get(target, key); } });
try {
if (process.argv.includes("--wait-for-slot")) {
process.stderr.write(`READY ${fixture.name}: database seeded, all real queries captured; press Enter to time\n`);
await new Promise<void>(resolve => { process.stdin.once("data", () => { process.stdin.pause(); resolve(); }); process.stdin.resume(); });
}
for (const { kind, before, after } of pairs) {
const samples = { before: [] as number[], after: [] as number[] };
const expected = await runQuery(timedDb, before);
assert.deepEqual(await runQuery(timedDb, after), expected, `${fixture.name}/${kind}: complete positional output differs`);
for (let n = 0; n < 7; n++) {
for (const arm of n % 2 ? ["after", "before"] as const : ["before", "after"] as const) {
const start = performance.now();
await runQuery(timedDb, arm === "before" ? before : after);
if (n >= 2) samples[arm].push(performance.now() - start);
}
}
const plans = {} as Record<"before" | "after", unknown>;
const actionLoops = {} as Record<"before" | "after", number>;
for (const arm of ["before", "after"] as const) {
const query = arm === "before" ? before : after;
const [row] = await connection.unsafe(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query.sql}`, query.params as never[]);
plans[arm] = row["QUERY PLAN"];
actionLoops[arm] = Math.max(0, ...nodes(row["QUERY PLAN"][0].Plan).filter(node => node["Relation Name"] === "chat_actions").map(node => Number(node["Actual Loops"])));
}
if (fixture.name === "retained-history") {
const oldPlan = (plans.before as { JIT?: unknown; Plan: Record<string, number> }[])[0]!;
const newPlan = (plans.after as { JIT?: unknown; Plan: Record<string, number> }[])[0]!;
const oldBuffers = Number(oldPlan.Plan["Shared Hit Blocks"] ?? 0) + Number(oldPlan.Plan["Shared Read Blocks"] ?? 0);
const newBuffers = Number(newPlan.Plan["Shared Hit Blocks"] ?? 0) + Number(newPlan.Plan["Shared Read Blocks"] ?? 0);
baselineExceedsWorkBudget ||= Boolean(oldPlan.JIT) || (kind === "deadline" && oldBuffers > 1000);
if (kind !== "ready" && newPlan.JIT) budgetFailures.push(`${kind}: unnecessary JIT on the retained-history fixture`);
if (kind === "deadline" && newBuffers > 1000) budgetFailures.push(`${kind}: ${newBuffers} shared buffer accesses exceeds fixture budget 1000`);
}
const median = (values: number[]) => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)]!;
results.push({ fixture, kind, equalPositionalRows: expected.length, before, after, samples, beforeMedianMs: median(samples.before), afterMedianMs: median(samples.after), actionLoops, plans });
process.stderr.write(`${fixture.name}/${kind}: ${median(samples.before).toFixed(2)} -> ${median(samples.after).toFixed(2)} ms; action scans ${actionLoops.before} -> ${actionLoops.after}\n`);
}
} finally { connection.release(); }
if (fixture.name === "small") {
const unrelated = await f.delivery({ state: "filtered" });
const other = await seedInboundDiscovery(db, 0);
for (const input of [
{ scope: "same-company-unrelated-delivery", companyId: f.companyId, endpointId: f.endpointId, deliveryId: unrelated.id },
{ scope: "other-company", companyId: other.companyId, endpointId: other.endpointId, deliveryId: other.pending.id },
{ scope: "null-delivery", companyId: f.companyId, endpointId: f.endpointId, deliveryId: null },
]) {
const action = await f.action(input.deliveryId, { companyId: input.companyId, endpointId: input.endpointId, result: { retryAt: "invalid timestamp" } });
const before = await captureInboundQuery(db, f, "ready", oldQueue.createChatConversationDeliveryQueue, oldService.chatChannelService, f.pending.id);
const after = await captureInboundQuery(db, f, "ready", undefined, undefined, f.pending.id);
const outcome = async (query: typeof before) => {
await db!.$client.unsafe("savepoint malformed_probe");
try { return { rows: await runQuery(db!, query) }; }
catch (error) { await db!.$client.unsafe("rollback to savepoint malformed_probe"); return { error: (error as { code?: string }).code }; }
finally { await db!.$client.unsafe("release savepoint malformed_probe"); }
};
const oldResult = await outcome(before), newResult = await outcome(after);
assert.deepEqual(newResult, oldResult, `${input.scope}: malformed ready selection outcome differs`);
malformedReadyControls.push({ scope: input.scope, before: oldResult, after: newResult });
await db.$client.unsafe("delete from chat_actions where id = $1::uuid", [action.id]);
}
}
} finally { await db.$client.unsafe("rollback"); }
await db.$client.unsafe("delete from chat_endpoints where id = $1::uuid", [f.endpointId]);
}
const runtimeSources = sources.map(source => ({ path: source,
baselineSha256: createHash("sha256").update(execFileSync("git", ["show", `${baselineRevision}:${source}`])).digest("hex"),
treatmentDiffSha256: createHash("sha256").update(execFileSync("git", ["diff", "--no-ext-diff", baselineRevision, "--", source])).digest("hex"),
}));
console.log(JSON.stringify({ baselineRevision, treatmentRevision: execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(), runtimeSources,
version, settings, jitOffControl: process.argv.includes("--jit-off"), baselineExceedsWorkBudget, budgetFailures,
malformedReadyControls, warmups: 2, timedSamplesPerArm: 5, setupAndProofMs: performance.now() - started, results }, null, 2));
if (process.argv.includes("--require-work-budget")) assert.deepEqual(budgetFailures, [], "Fixture work budget failed");
} finally {
try { await db?.$client.end({ timeout: 0 }); await database?.cleanup(); await fs.rm(dir, { recursive: true, force: true }); }
finally { clearInterval(keepAlive); }
}