Files
PaperClipAI/server/src/services/delivery-work-coordinator.ts
T
DottaandPaperclip e40de0c9e8 perf(ci): refresh test weights and narrow graph dependencies (#18464)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Contributors need fast, reliable CI feedback.
> - Diff-aware CI selects test files and partitions them into runner
jobs.
> - Stale cost estimates put several slow suites on the same runner.
Broad imports also couple small tests to large modules.
> - This PR refreshes test costs, runs independent graph discovery in
parallel, and narrows three dependency boundaries.
> - Selection safeguards and CODEOWNER-only AWS routing stay in force.

## Linked Issues or Issue Description

Refs #18398. This follows the audit of [PR #18427's verification
run](https://github.com/paperclipai/paperclip/actions/runs/41169978784).

The audit found 403 selected general-server files without timing
weights. Three suites measured at 232s, 122s and 116s shared one shard
because each received a roughly 2s fallback weight. Local graph
discovery across four projects took 43.7s serially and 21.7s with two
workers. A reverse-order repeat took 37.7s serially and 22.6s with two
workers. Both comparisons produced identical selection results. This is
a local measurement, not a claim about production CI speed.

## What Changed

- Refresh general-server costs for 1,285 of 1,309 current files and
serialized costs for all 163 files from two successful protected-master
Cloud readiness runs. Retain prior costs for unmeasured files, including
dedicated chat and native-runner suites.
- Record passing server file costs, including collection, setup and
environment preparation. A weekly hosted workflow validates trusted
source runs and opens a timing-only PR. It tests shard coverage, pauses
while a refresh PR is open, and never merges automatically.
- Discover at most two project graphs concurrently in isolated,
heap-limited Node processes. Cache repeated inventory reads within one
selection. Worker errors and timeouts retain the full task plan.
- Fetch only the merge commit and parents for the selector. A real
shallow-clone regression proves the complete diff still includes more
than 300 files, renames and deletions.
- Share the identical log viewer and run details between both
AgentDetail pages. The viewer test imports the shared implementation and
runs each of its 13 distinct cases once.
- Extract the pure API error class while preserving the API client's
public exports. Import delivery work signals through their existing
narrow DB export.

## Verification

- PASS: `pnpm -r typecheck` and `pnpm build`.
- PASS: 284 CI selection, timing, shallow-diff, sharding and routing
tests.
- PASS: 74 further cache, routing and graph-proof tests.
- PASS: 94 targeted UI assertions across the viewer, run invocation, API
errors/client and progress helpers. All 76 assertions in the 12 server
suites made independently selectable also pass.
- PASS: `pnpm check:token-gates`, module-boundary checks, YAML parsing
and `git diff --check`.
- PASS: [Full PR CI and required `ci / verify`
aggregate](https://github.com/paperclipai/paperclip/actions/runs/41206683345),
including all 12 general-server shards, all nine serialized shards,
workspace groups, chat suites, Runner lanes and build checks.
- PASS: [GitHub hosted real-workspace selector and cache/routing
qualification](https://github.com/paperclipai/paperclip/actions/runs/41206683205).
- PASS: Both Greptile and Paperclip reviews are 5/5 with no actionable
findings or inline comments on `7dc65c51b8`. The PR has no merge
conflicts.
- PASS: The production refresh parser accepted all 107 structured
file-cost records from a real successful server CI job, including
setup/loading cost.
- Local `pnpm test:run` completed all six chat shards successfully. The
duplicate monolithic local run was stopped after the complete remote
suite passed; it was not completed locally.

The focused log-viewer command improved from 18.84s (26 duplicate
variant cases) to 8.49s (all 13 distinct behavior cases). The dependency
audit confirms 12 server files and the API-error test no longer need
conservative always-run retention. Repartitioning the audited PR with
the new costs changes the slowest shard from 810s to 591s of summed
historical test execution (1,124 of 1,142 files measured). This is a
workload simulation, not measured new CI wall time.

## Risks

- Two graph processes use more peak memory. Each has a 2 GiB heap limit
and a three-minute timeout. Any failure preserves the original task
plan. Real workspace qualification runs in GitHub CI.
- Historical logs contain test/hook costs only. New reporter records add
loading/setup costs after merge. The manifests identify measurement
coverage and source runs. These weights change placement, never
permission to skip tests.
- Runner load adds timing noise. The refresh uses medians and requires
at least 90% measurement coverage in each lane.
- GitHub may require approval to run checks on a PR created with
`GITHUB_TOKEN`. Refreshes remain reviewable and do not merge themselves.
- The shared viewer is a mechanical extraction. Both pages retain their
existing exports, and the behavior assertions remain covered. AWS runner
authorization is unchanged.

## Model Used

OpenAI Codex, GPT-6, with repository inspection, reasoning, code
execution and GitHub tools. The exact deployment ID and context-window
size were not exposed in this session.

## 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 references)
- [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 (focused checks and six
chat shards; complete suite passed in CI as detailed above)
- [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-31 11:05:12 -05:00

138 lines
6.4 KiB
TypeScript

import { subscribeDatabaseWork, databaseWorkPending, reconcileDatabaseWork } from "@paperclipai/db/work-signals";
import { createWorkScheduler } from "./work-scheduler.js";
import { type DeliveryQueue } from "./delivery-work-notifications.js";
import { beginIdleTrackedWork } from "./task-admission.js";
export function createDeliveryWorkCoordinator(input: {
owner: object;
canRun: () => boolean;
// Idle drain may reconcile admitted writes without dispatching new work.
// Warm standby can disable both operations.
canReconcile?: () => boolean;
onError: (error: unknown, queue: DeliveryQueue) => void;
}) {
const scheduler = createWorkScheduler();
const workers = new Map<DeliveryQueue, { wake: () => void; stop: () => Promise<void> }>();
let stopped = false;
return {
// Only these registered tasks are represented; not yet the whole instance.
nextWakeAt: scheduler.nextWakeAt,
register(queue: DeliveryQueue, task: {
retryMs: number;
run: (signal: AbortSignal) => Promise<unknown>;
hasPending: () => Promise<boolean>;
// Scoped hints belong to the outer transaction, including uncertainty recovery.
onSettled?: (hints?: ReadonlySet<string>) => void;
// Optional maintenance/retry deadline restored from durable state.
nextRunAt?: () => number | null;
// Opt in only when run has recorded per-item failure backoffs. Other
// eligible items may already be due, unlike an unchanged failed deadline.
nextRunAtOnError?: () => number | null;
// An empty queue's maintenance may wait until dispatch admission reopens.
// Notifications and unresolved producer writes still reconcile in drain.
pausePassiveMaintenance?: boolean;
// Accounting and stop delivery finish already-admitted obligations;
// they must continue during admission drain, never warm standby.
runDuringDrain?: boolean;
}) {
if (stopped) throw new Error("Delivery coordinator is stopped");
if (workers.has(queue)) throw new Error(`Delivery worker already registered: ${queue}`);
let dirty = false;
let scheduledAt: number | null = null;
let running: Promise<void> | null = null;
let workerStopped = false;
let controller: AbortController | null = null;
let finishIntent: (() => void) | undefined;
let pendingWork = true;
function schedule(delay: number, deadline?: number) {
if (workerStopped) return;
const at = deadline ?? Date.now() + delay;
if (scheduledAt !== null && scheduledAt <= at) return;
scheduledAt = at;
scheduler.schedule(queue, at, start);
}
function start() {
if (workerStopped || running) return;
scheduler.cancel(queue);
scheduledAt = null;
if (!input.canRun() && !(input.canReconcile?.() ?? false)) { schedule(task.retryMs); return; }
if (!input.canRun() && task.pausePassiveMaintenance && !pendingWork && !dirty &&
!finishIntent && !databaseWorkPending(input.owner, queue)) {
// A timer alone must not change the idle generation during a slow
// backup. Retry without querying or admitting work; a notification
// still preempts this deadline and retains its normal intent hold.
schedule(task.retryMs);
return;
}
dirty = false;
const finish = beginIdleTrackedWork();
const attempt = new AbortController();
let runFailed = false;
controller = attempt;
running = Promise.resolve().then(() => reconcileDatabaseWork(input.owner, queue))
.then(() => workerStopped || !(input.canRun() || (task.runDuringDrain && input.canReconcile?.())) ? undefined : task.run(attempt.signal).catch(error => { runFailed = true; throw error; })).then(() => workerStopped ? false : task.hasPending()).then(pending => {
pendingWork = pending || databaseWorkPending(input.owner, queue);
if (pendingWork) schedule(task.retryMs);
else { finishIntent?.(); finishIntent = undefined; }
const next = task.nextRunAt?.();
if (next != null) {
const delay = Math.max(0, next - Date.now());
schedule(delay === 0 && !input.canRun() ? task.retryMs : delay, delay > 0 ? next : undefined);
}
}).catch(error => {
schedule(task.retryMs);
let failure = error;
try {
const scopedRetry = runFailed && task.nextRunAtOnError;
const next = input.canRun() ? (scopedRetry ? scopedRetry() : task.nextRunAt?.()) : null;
if (next != null && (scopedRetry || next > Date.now())) schedule(0, Math.max(Date.now(), next));
} catch (deadlineError) {
failure = new AggregateError([error, deadlineError], "Delivery failure deadline could not be read");
}
// Logging must never turn a recoverable sweep failure into an
// unhandled rejection or prevent other queues from running.
try { if (!workerStopped) input.onError(failure, queue); } catch { /* Recovery remains scheduled. */ }
}).finally(() => {
running = null;
controller = null;
finish();
if (dirty) schedule(0);
});
}
function wake() {
if (workerStopped) return;
dirty = true;
if (!running) schedule(0);
}
const unsubscribe = subscribeDatabaseWork(input.owner, queue, (event, hints) => {
if (workerStopped) return;
if (databaseWorkPending(input.owner, queue)) {
finishIntent ??= beginIdleTrackedWork();
// Give the transaction time to settle. Its settlement notification
// replaces this deadline with an immediate wake on the normal path.
if (!running) schedule(task.retryMs);
}
if (event === "settled") { task.onSettled?.(hints); wake(); }
});
const worker = { wake, async stop() {
workerStopped = true;
controller?.abort(new Error("Delivery worker stopped"));
unsubscribe();
scheduler.cancel(queue);
await running;
finishIntent?.(); finishIntent = undefined;
} };
workers.set(queue, worker);
const ready = Promise.resolve().then(() => { start(); return running; });
return { ready, ...worker };
},
async stop() {
stopped = true;
scheduler.stop();
await Promise.all(Array.from(workers.values(), worker => worker.stop()));
workers.clear();
},
};
}