Files
PaperClipAI/scripts/benchmark-chat-native-progress.mjs
T
DottaandPaperclip a7504784e5 perf(chat): avoid snapshot reads in native progress polls (#16845)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Chat connectors publish safe progress from native runs.
> - A recurring query joins each run to its conversation through JSON
context.
> - Large snapshots make repeated JSON reads expensive.
> - Issue-scoped runs already have a typed issue binding.
> - This pull request uses that binding and retains the legacy JSON
fallback.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The native chat-progress candidate query in
`enqueueSafeNativeChatProgress`.

**Subsystem affected**

`server/` database queries and chat publication.

**Current behavior**

The conversation join extracts `issueId` from each run's JSON snapshot.
A local plan probes the run repeatedly and reads a large snapshot for
each probe.

**Proposed behavior**

Use the run's typed `issue_id` when present. Read the JSON key when that
column is null. Keep the existing company, endpoint, event, publication,
cursor, and cadence conditions.

**Reason and benefit**

Avoid repeated snapshot reads for scoped runs. Related: #15802 changed
delivery queue scheduling; it left native progress projection on its
existing cadence.

## What Changed

- Change only the native-progress conversation join. Leave the separate
milestone query, which already uses its expression index, unchanged.
- Add a disposable PostgreSQL benchmark that captures the real service
SQL and compares complete rows on one shared fixture. It includes a
legacy null binding, unrelated tenants, 2,400 historical decoys, and 144
KiB of random bytes per snapshot (about 192 KiB after base64 encoding).
- Compare cardinality across independent fixtures. Their generated
conversation and event IDs are not deterministic.
- Add PostgreSQL regressions for a typed issue binding without the JSON
key and for the legacy NULL-column fallback.

## Verification

- Full-size paired local measurement: native baseline 57.67 ms median,
treatment 19.35 ms median, **2.98× faster**. Both return the same 41
complete rows. Five timed samples follow warmups, with baseline and
treatment alternated. The unchanged milestone query is a control.
- An independent full-size run also passed complete row equality and
measured 65.22 → 24.08 ms (2.71×).
- The small 1 KiB control passed the same 41-row equality check and was
neutral: 15.20 → 15.45 ms median. The improvement targets large
snapshots, not small payloads. A repeat of the final JavaScript entry
point also passed equality and measured 2.97× while the full test suite
was running; its wider timing spread is not the primary performance
result.
- Four PostgreSQL cases pass: typed issue scope without a legacy JSON
key, NULL-column JSON fallback, native-progress cadence, and
paused-endpoint no-replay behavior. The remaining tests in that file
were excluded by the name filter.
- Reproduce with `node --import ./cli/node_modules/tsx/dist/loader.mjs
scripts/benchmark-chat-native-progress.mjs --phase=baseline` before
applying the query change. Run the same command with `--phase=treatment`
after applying it. The tool stores SQL, parameters, plans, all samples,
and equality evidence under `.scratch/`.
- Full workspace typecheck and final server typecheck passed locally.
Four focused PostgreSQL cases passed. The duplicate unsharded local
full-suite run was stopped after the same commit passed all CI test
shards; the full local tests/build sequence was not completed. CI Build
and the aggregate verification gate passed on that commit.
- Current-head CI is green at `82c9ef9` (55 successful checks, two valid
skips). An unchanged runtime-supervision test failed in the first
attempt; its isolated local reproduction passed, and one failed-job
rerun passed on the same commit. The local reproduction runs on macOS
and does not exercise the Linux process-identity branch.
- Greptile reviewed `82c9ef9` at 5/5 with no unresolved threads.

## Risks

- The typed issue binding takes precedence when present; older rows
retain JSON fallback. Company and publication ownership checks remain in
the query.
- Measurements use local embedded PostgreSQL 18 with synthetic data. The
hosted motivation used PostgreSQL 17. Absolute timings and plans can
differ on other compute and data distributions.
- The milestone benchmark returns an empty candidate set and is a
performance control; the nonempty native comparison is the correctness
and performance target.
- No schema migration or provider network request is added. This PR does
not deploy a query change to a live database.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository inspection, tool
use, and code execution. The exact serving model ID and context-window
size are not exposed. Z-AI GLM 5.3 Prime through OpenRouter
(`openrouter/z-ai/glm-5.3-prime`) assisted the initial query treatment
and benchmark. Codex repaired the comparison, narrowed the production
change, and ran the full-size proof.

## 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 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-19 10:20:32 -05:00

437 lines
18 KiB
JavaScript

import assert from "node:assert/strict";
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
companies,
agents,
issues,
createDb,
startEmbeddedPostgresTestDatabase
} from "../packages/db/src/index.js";
const phase = process.argv.find((arg) => arg.startsWith("--phase="))?.slice(8) ?? "baseline";
assert.ok(phase === "baseline" || phase === "treatment", "--phase must be baseline or treatment");
const ANCHOR_MS = Date.parse("2026-10-19T00:00:00.000Z");
const SINCE_MS = ANCHOR_MS - 24 * 60 * 6e4;
const PROMPT_KIB = 144;
function mulberry32(seed2) {
return () => {
seed2 |= 0;
seed2 = seed2 + 1831565813 | 0;
let t = Math.imul(seed2 ^ seed2 >>> 15, 1 | seed2);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const rand = mulberry32(1346457664);
const hex = (n) => Array.from({ length: n }, () => "0123456789abcdef"[Math.floor(rand() * 16)]).join("");
const uuid = () => `${hex(8)}-${hex(4)}-4${hex(3)}-${"89ab"[Math.floor(rand() * 4)]}${hex(3)}-${hex(12)}`;
const prompt = Buffer.from(
Array.from({ length: PROMPT_KIB * 1024 }, () => Math.floor(rand() * 256))
).toString("base64");
const iso = (ms) => new Date(ms).toISOString();
async function seed(db) {
const companyA = uuid();
const companyB = uuid();
const agentA = uuid();
const agentB = uuid();
const endpointA = uuid();
const endpointB = uuid();
await db.insert(companies).values([
{ id: companyA, name: "Synthetic chat polls A", issuePrefix: "BENCH" },
{ id: companyB, name: "Synthetic chat polls B", issuePrefix: "BNCH" }
]);
await db.insert(agents).values([
{ id: agentA, companyId: companyA, name: "Fixture A", adapterType: "codex_local" },
{ id: agentB, companyId: companyB, name: "Fixture B", adapterType: "codex_local" }
]);
const conversationIssueIds = [];
const issueIdsA = [];
for (let n = 0; n < 50; n++) {
const issueId = uuid();
if (n < 45) conversationIssueIds.push(issueId);
issueIdsA.push(issueId);
await db.insert(issues).values({ id: issueId, companyId: companyA, title: `Fixture issue A${n}` });
}
const conversationIssueIdsB = [];
for (let n = 0; n < 10; n++) {
const issueId = uuid();
conversationIssueIdsB.push(issueId);
await db.insert(issues).values({ id: issueId, companyId: companyB, title: `Fixture issue B${n}` });
}
for (const [companyId, endpointId, agentId, ids, label] of [
[companyA, endpointA, agentA, conversationIssueIds, "A"],
[companyB, endpointB, agentB, conversationIssueIdsB, "B"]
]) {
const applicationId = uuid();
const connectionId = uuid();
await db.$client.unsafe(
`INSERT INTO tool_applications
(id, company_id, name, type, status)
VALUES ($1::uuid, $2::uuid, $3, 'chat', 'active')`,
[applicationId, companyId, `fixture-app-${label}`]
);
await db.$client.unsafe(
`INSERT INTO tool_connections
(id, company_id, application_id, name, uid, connection_purpose, transport, status, enabled)
VALUES ($1::uuid, $2::uuid, $3::uuid, $4, $5, 'channel', 'chat_sdk', 'active', true)`,
[connectionId, companyId, applicationId, `fixture-connection-${label}`, `fixture-conn-${label}`]
);
await db.$client.unsafe(
`INSERT INTO chat_endpoints
(id, company_id, connection_id, provider, public_id, publication_mode, assigned_agent_id, status)
VALUES ($1::uuid, $2::uuid, $3::uuid, 'slack', $4, 'automatic', $5::uuid, 'active')`,
[endpointId, companyId, connectionId, `fixture-endpoint-${label}`, agentId]
);
await db.$client.unsafe(
`INSERT INTO chat_conversations
(id, company_id, endpoint_id, issue_id, external_conversation_id, external_label, state)
SELECT gen_random_uuid(), $1::uuid, $2::uuid, issue::uuid, 'conv-' || n, 'Conv ${label} ' || n,
CASE WHEN n % 9 = 0 THEN 'waiting' WHEN n % 11 = 0 THEN 'completed' ELSE 'active' END
FROM (SELECT issue, row_number() OVER () AS n FROM unnest($3::uuid[]) AS issue) sub`,
[companyId, endpointId, ids]
);
}
const nativeRunIds = [];
const convRowsA = conversationIssueIds.map((issue, n) => ({ issue, n }));
for (const { issue, n } of convRowsA) {
const nativeRunId = uuid();
nativeRunIds.push(nativeRunId);
const base = ANCHOR_MS - n % 40 * 6e4;
await db.$client.unsafe(
`INSERT INTO heartbeat_runs
(id, company_id, agent_id, scope_kind, issue_id, runtime_mode, status, error_code, created_at, updated_at, context_snapshot, result_json)
VALUES ($1::uuid, $2::uuid, $3::uuid, 'issue', $4::uuid, 'native', 'running', NULL, $5, $5,
jsonb_build_object('issueId', $4::text, 'source', 'chat:slack:fixture', 'prompt', $6::text), NULL)`,
[nativeRunId, companyA, agentA, issue, iso(base), prompt]
);
await db.$client.unsafe(
`INSERT INTO heartbeat_runs
(id, company_id, agent_id, scope_kind, runtime_mode, status, error_code, created_at, updated_at, context_snapshot, result_json)
VALUES ($1::uuid, $2::uuid, $3::uuid, 'company', 'legacy',
CASE WHEN $4 % 5 = 0 THEN 'failed' WHEN $4 % 7 = 0 THEN 'succeeded' ELSE 'running' END,
CASE WHEN $4 % 5 = 0 THEN 'execution_reconciliation_required' END, $5, $5,
jsonb_build_object('issueId', $6::text, 'source', 'chat:slack:fixture', 'prompt', $7::text),
CASE WHEN $4 % 7 = 0 THEN jsonb_build_object('presentationDecision', jsonb_build_object('kind', 'fixture')) END)`,
[uuid(), companyA, agentA, n, iso(base), issue, prompt]
);
await db.$client.unsafe(
`INSERT INTO chat_publications
(company_id, endpoint_id, conversation_id, issue_id, idempotency_key, payload, state, created_at)
VALUES ($1::uuid, $2::uuid,
(SELECT id FROM chat_conversations WHERE company_id = $1::uuid AND issue_id = $4::uuid LIMIT 1),
$4::uuid, 'run:' || $3::text || ':working:' || $2::text,
jsonb_build_object('classification', 'external', 'source', 'safe_milestone', 'text', 'working'),
'streaming', $5)`,
[companyA, endpointA, nativeRunId, issue, iso(base - 12e4)]
);
await db.$client.unsafe(
`INSERT INTO heartbeat_run_events
(company_id, run_id, agent_id, seq, event_type, message, created_at)
VALUES ($1::uuid, $2::uuid, $3::uuid, 1, 'workspace.ready', 'ready', $4),
($1::uuid, $2::uuid, $3::uuid, 2, 'tool.execution.started', 'tool', $5)`,
[companyA, nativeRunId, agentA, iso(base + 3e4), iso(base + 45e3)]
);
}
for (const [n, issue] of conversationIssueIdsB.entries()) {
const runId = uuid();
const base = ANCHOR_MS - n % 10 * 6e4;
await db.$client.unsafe(
`INSERT INTO heartbeat_runs
(id, company_id, agent_id, scope_kind, issue_id, runtime_mode, status, error_code, created_at, updated_at, context_snapshot)
VALUES ($1::uuid, $2::uuid, $3::uuid, 'issue', $4::uuid, 'native', 'running', NULL, $5, $5,
jsonb_build_object('issueId', $4::text, 'source', 'chat:slack:fixture', 'prompt', $6::text))`,
[runId, companyB, agentB, issue, iso(base), prompt]
);
}
for (let start = 0; start < 2400; start += 400) {
await db.$client.unsafe(
`INSERT INTO heartbeat_runs
(company_id, agent_id, scope_kind, status, error_code, created_at, updated_at, context_snapshot)
SELECT $1::uuid, $2::uuid, 'company',
(ARRAY['queued','running','succeeded','interrupted','failed','timed_out','cancelled'])[(n % 7) + 1],
NULL,
make_interval(hours => CASE WHEN n % 3 = 0 THEN -(30 + (n % 100)) ELSE -(n % 16) END, secs => -60) + $4::timestamptz,
make_interval(hours => CASE WHEN n % 3 = 0 THEN -(30 + (n % 100)) ELSE -(n % 16) END, secs => -60) + $4::timestamptz,
jsonb_build_object('issueId', gen_random_uuid()::text, 'source', 'chat:slack:fixture', 'prompt', $3::text)
FROM generate_series(${start + 1}, ${start + 400}) n`,
[companyA, agentA, prompt, iso(ANCHOR_MS)]
);
}
await db.$client.unsafe(
`INSERT INTO heartbeat_runs
(company_id, agent_id, scope_kind, status, error_code, created_at, updated_at, context_snapshot)
SELECT $1::uuid, $2::uuid, 'company',
(ARRAY['queued','running','succeeded','interrupted','failed','timed_out','cancelled'])[(n % 7) + 1],
NULL,
make_interval(hours => CASE WHEN n % 3 = 0 THEN -(30 + (n % 100)) ELSE -(n % 16) END, secs => -60) + $4::timestamptz,
make_interval(hours => CASE WHEN n % 3 = 0 THEN -(30 + (n % 100)) ELSE -(n % 16) END, secs => -60) + $4::timestamptz,
jsonb_build_object('issueId', gen_random_uuid()::text, 'source', 'chat:slack:fixture', 'prompt', $3::text)
FROM generate_series(1, 100) n`,
[companyB, agentB, prompt, iso(ANCHOR_MS)]
);
await db.$client.unsafe(
"UPDATE heartbeat_runs SET scope_kind = 'company', issue_id = NULL WHERE id = $1::uuid",
[nativeRunIds[0]]
);
await db.$client.unsafe("ANALYZE heartbeat_runs");
await db.$client.unsafe("ANALYZE chat_conversations");
await db.$client.unsafe("ANALYZE heartbeat_run_events");
await db.$client.unsafe("ANALYZE chat_publications");
return { companyA, companyB };
}
function recordingDb(db, sink) {
return new Proxy(db, {
get(target, key) {
if (key !== "select") return Reflect.get(target, key);
return (...fields) => {
const builder = target.select(...fields);
const from = builder.from.bind(builder);
builder.from = (...args) => {
const selection = from(...args);
const execute = selection.execute.bind(selection);
selection.execute = async () => {
const captured = selection.toSQL();
sink.push({ sql: captured.sql, params: captured.params });
return execute();
};
return selection;
};
return builder;
};
}
});
}
async function capture(db) {
const sink = [];
const module = await import("../server/src/services/chat-run-publications.js");
await module.enqueueChatRunMilestones(recordingDb(db, sink), {
since: new Date(SINCE_MS),
limit: 200
});
const milestone = sink.find(
(q) => q.sql.startsWith('select "heartbeat_runs"."id", "heartbeat_runs"."status"') && q.sql.includes('inner join "chat_conversations"')
);
const native = sink.find((q) => q.sql.startsWith('select "heartbeat_run_events"."id", "heartbeat_run_events"."seq"'));
assert.ok(milestone, "milestone candidate query was not captured");
assert.ok(native, "native progress candidate query was not captured");
assert.match(milestone.sql, /chat_conversations/, "milestone query lacks the conversations join");
assert.match(native.sql, /chat_conversations/, "native query lacks the conversations join");
return { milestone, native };
}
async function measureOnce(client, query) {
const start = performance.now();
const rows = await client.unsafe(query.sql, query.params).values();
return { ms: performance.now() - start, rows };
}
async function measure(client, query, rounds = 7) {
const samples = [];
let rows = [];
for (let round = 0; round < rounds; round++) {
const result = await measureOnce(client, query);
rows = result.rows;
if (round >= 2) samples.push(result.ms);
}
const [plan] = await client.unsafe(
`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query.sql}`,
query.params
);
samples.sort((a, b) => a - b);
return { samples, rows, plan };
}
const normalize = (value) => value instanceof Date ? { __date: value.toISOString() } : Array.isArray(value) ? value.map(normalize) : value !== null && typeof value === "object" ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, normalize(v)])) : value;
function planSummary(plan) {
const nodes = [];
const wrapped = plan?.["QUERY PLAN"];
const raw = wrapped ?? plan;
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
const root = Array.isArray(parsed) ? parsed[0] : parsed;
const planNode = root?.["Plan"] ?? root;
assert.ok(planNode, "explain plan lacks a Plan node");
const walk = (node) => {
nodes.push({
node_type: node["Node Type"],
actual_rows: node["Actual Rows"],
loops: node["Actual Loops"] ?? 1,
shared_hit: node["Shared Hit Blocks"],
shared_read: node["Shared Read Blocks"]
});
for (const child of node["Plans"] ?? []) walk(child);
};
walk(planNode);
return { root_node: planNode?.["Node Type"], node_count: nodes.length, nodes: nodes.slice(0, 12) };
}
const median = (values) => values.slice().sort((a, b) => a - b)[Math.floor(values.length / 2)];
const keepAlive = setInterval(() => {
}, 1e3);
let database;
const databases = [];
async function withDatabase(fn) {
database = await startEmbeddedPostgresTestDatabase(`chat-poll-bench-${phase}-`);
databases.push(database);
const db = createDb(database.connectionString);
try {
return await fn(db);
} finally {
await db.$client.end();
await database.cleanup();
}
}
try {
const scratchDir = path.join(fileURLToPath(new URL("../.scratch/", import.meta.url)));
await fs.mkdir(scratchDir, { recursive: true });
const baselinePath = path.join(scratchDir, "chat-native-progress-baseline.json");
const reportPath = path.join(scratchDir, "chat-native-progress-report.json");
const captured = await withDatabase((db) => seed(db).then(() => capture(db)));
if (phase === "baseline") {
const result = await withDatabase(async (db) => {
await seed(db);
const milestone = await measure(db.$client, captured.milestone);
const native = await measure(db.$client, captured.native);
return { milestone, native };
});
const payload = {
anchor: new Date(ANCHOR_MS).toISOString(),
since: new Date(SINCE_MS).toISOString(),
promptKiB: PROMPT_KIB,
fixture: {
conversationIssues: 45,
candidateRunsPerIssue: 2,
decoyRuns: 2400,
crossCompanyDecoys: 100,
nativeRunsWithEvents: 45
},
milestone: {
sql: captured.milestone.sql,
params: captured.milestone.params,
samples: result.milestone.samples,
medianMs: median(result.milestone.samples),
rows: normalize(result.milestone.rows),
plan: planSummary(result.milestone.plan),
fullPlan: result.milestone.plan
},
native: {
sql: captured.native.sql,
params: captured.native.params,
samples: result.native.samples,
medianMs: median(result.native.samples),
rows: normalize(result.native.rows),
plan: planSummary(result.native.plan),
fullPlan: result.native.plan
}
};
await fs.writeFile(baselinePath, JSON.stringify(payload, null, 2));
console.log(
JSON.stringify(
{
phase,
milestone: {
medianMs: payload.milestone.medianMs,
samples: payload.milestone.samples,
rows: result.milestone.rows.length,
plan: payload.milestone.plan.root_node
},
native: {
medianMs: payload.native.medianMs,
samples: payload.native.samples,
rows: result.native.rows.length,
plan: payload.native.plan.root_node
}
},
null,
2
)
);
} else {
const baseline = JSON.parse(await fs.readFile(baselinePath, "utf8"));
const report = await withDatabase(async (db) => {
await seed(db);
const client = db.$client;
const pairs = {
milestone: { baseline: { sql: baseline.milestone.sql, params: baseline.milestone.params }, treatment: captured.milestone },
native: { baseline: { sql: baseline.native.sql, params: baseline.native.params }, treatment: captured.native }
};
const measurements = {
milestone: { baseline: [], treatment: [] },
native: { baseline: [], treatment: [] }
};
const rows = {
milestone: { baseline: [], treatment: [] },
native: { baseline: [], treatment: [] }
};
for (let round = 0; round < 7; round++) {
for (const name of ["milestone", "native"]) {
const order = round % 2 === 0 ? ["baseline", "treatment"] : ["treatment", "baseline"];
for (const side of order) {
const query = pairs[name][side];
const result = await measureOnce(client, query);
rows[name][side] = result.rows;
if (round >= 2) measurements[name][side].push(result.ms);
}
}
}
const plans = {
milestone: { baseline: null, treatment: null },
native: { baseline: null, treatment: null }
};
for (const name of ["milestone", "native"]) {
for (const side of ["baseline", "treatment"]) {
const [plan] = await client.unsafe(
`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${pairs[name][side].sql}`,
pairs[name][side].params
);
plans[name][side] = planSummary(plan);
}
}
return { measurements, rows, plans };
});
for (const name of ["milestone", "native"]) {
assert.deepEqual(
normalize(report.rows[name].treatment),
normalize(report.rows[name].baseline),
`${name} poll rows changed between baseline and treatment`
);
assert.equal(
report.rows[name].baseline.length,
baseline[name].rows.length,
`${name} poll baseline cardinality drifted across fixtures`
);
if (name === "native") assert.ok(report.rows[name].baseline.length > 0, "native equality must cover actual rows");
}
const summary = {};
for (const name of ["milestone", "native"]) {
for (const side of ["baseline", "treatment"]) {
summary[`${name}:${side}`] = {
medianMs: median(report.measurements[name][side]),
samples: report.measurements[name][side],
rows: report.rows[name][side].length
};
}
}
const output = {
phase,
fixture: {
promptKiB: PROMPT_KIB,
windowHours: 24,
decoyRuns: 2400,
crossCompanyDecoys: 100
},
summary,
plans: report.plans,
speedup: {
milestone: summary["milestone:baseline"].medianMs / summary["milestone:treatment"].medianMs,
native: summary["native:baseline"].medianMs / summary["native:treatment"].medianMs
},
equalRows: true
};
await fs.writeFile(reportPath, JSON.stringify(output, null, 2));
console.log(JSON.stringify({ ...output, plans: void 0 }, null, 2));
}
} finally {
clearInterval(keepAlive);
for (const database2 of databases) {
await database2.cleanup().catch(() => {
});
}
}