mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - When the server starts, `/api/health` answers "starting" until startup recovery finishes, and deploy tooling waits for "ok" > - Startup recovery waits for the first sweep of the terminal-workspace worker > - #16304 added workspace compaction to that sweep. Compaction walks worktrees, runs Git, and deletes dependency trees, and it re-checks every eligible workspace after a restart > - Compaction is never needed to serve requests, so it should not delay readiness > - This pull request skips compaction in the startup sweep only; later sweeps compact as before > - The benefit is that a compaction backlog cannot make a restart slower ## Linked Issues or Issue Description Follow-up to #16304. **What existing behavior does this improve?** Server startup time after #16304. **Current behavior** `await terminalWorkspaceWorker.ready` blocks the readiness flip until the first terminal-workspace sweep ends. That sweep compacts every eligible workspace in its page. The per-cycle compaction memo is in memory, so after each restart every eligible workspace is walked and checked with Git again, and any directories found are deleted before the sweep ends. **Proposed behavior** The first sweep skips compaction. Pending compactions keep their deadlines, so the worker wakes and a later sweep compacts them. **Reason and benefit** Readiness depends only on work that is needed to serve requests. On one measured instance the first sweep took 5–8 s with compaction; with a larger backlog, compaction could add seconds per workspace. **Breaking changes** None. Compaction happens one sweep later after a restart. ## What Changed - `sweepTerminalWorkspaces(limit, signal, { compact })`: `compact: false` skips compaction for that sweep and keeps each pending compaction deadline. - `registerTerminalWorkspaceWork` passes `compact: false` to its first sweep (the one readiness waits for) and `compact: true` after that. - `server/src/index.ts` forwards the option through the logging wrapper. - `doc/DEVELOPING.md`: the compaction section says the startup sweep never compacts. ## Verification - `pnpm exec vitest run src/__tests__/terminal-workspace-work.test.ts src/__tests__/execution-workspace-compaction.test.ts src/__tests__/workspace-compaction.test.ts` (in `server/`): 35 passed. - New: the worker passes `compact: false` only to the startup sweep and `compact: true` to the next one. - New: a sweep with `compact: false` leaves the files in place, the compaction deadline stays due, and the next sweep compacts. - `pnpm typecheck` in `server/`: passed. ## Risks - Low risk. After a restart, compaction runs a few seconds later than before. Archive behavior does not change. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), 1M context, through Claude Code with tool use (shell, file edits, test runs). ## 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [ ] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
55 lines
2.2 KiB
TypeScript
55 lines
2.2 KiB
TypeScript
import type { createDeliveryWorkCoordinator } from "./delivery-work-coordinator.js";
|
|
import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notifications.js";
|
|
|
|
export const TERMINAL_WORKSPACE_RETRY_MS = 5 * 60 * 1000;
|
|
|
|
export function notifyTerminalWorkspaceWork(transaction: object, hint?: string) {
|
|
return notifyDeliveryWork(transaction, DELIVERY_QUEUES.terminalWorkspaces, hint);
|
|
}
|
|
|
|
export function registerTerminalWorkspaceWork(
|
|
coordinator: ReturnType<typeof createDeliveryWorkCoordinator>,
|
|
service: {
|
|
sweepTerminalWorkspaces(
|
|
limit?: number,
|
|
signal?: AbortSignal,
|
|
options?: { compact?: boolean },
|
|
): Promise<{ checked: number }>;
|
|
nextTerminalWorkspaceSweepAt(): Promise<number | null>;
|
|
hasTerminalWorkspaceSweepContinuation(): boolean;
|
|
terminalWorkspaceWorkSettled?(hints?: ReadonlySet<string>): void;
|
|
},
|
|
canRun: () => boolean,
|
|
) {
|
|
let next: number | null = Date.now();
|
|
let recovered = false;
|
|
// Server readiness waits for the first sweep. Compaction walks worktrees and
|
|
// deletes dependency trees, which can take seconds per workspace and is never
|
|
// needed to serve requests, so the first sweep skips it and later sweeps do it.
|
|
let startupSweepStarted = false;
|
|
const worker = coordinator.register(DELIVERY_QUEUES.terminalWorkspaces, {
|
|
retryMs: TERMINAL_WORKSPACE_RETRY_MS,
|
|
onSettled: service.terminalWorkspaceWorkSettled,
|
|
run: signal => {
|
|
recovered = false;
|
|
next = Date.now() + TERMINAL_WORKSPACE_RETRY_MS;
|
|
const compact = startupSweepStarted;
|
|
startupSweepStarted = true;
|
|
return service.sweepTerminalWorkspaces(50, signal, { compact });
|
|
},
|
|
hasPending: async () => {
|
|
if (!canRun()) next = Date.now() + TERMINAL_WORKSPACE_RETRY_MS;
|
|
else if (service.hasTerminalWorkspaceSweepContinuation()) next = Date.now() + 1000;
|
|
else {
|
|
const deadline = await service.nextTerminalWorkspaceSweepAt();
|
|
next = deadline === null ? null : deadline > Date.now()
|
|
? deadline : Date.now() + TERMINAL_WORKSPACE_RETRY_MS;
|
|
recovered = true;
|
|
}
|
|
return false;
|
|
},
|
|
nextRunAt: () => next,
|
|
});
|
|
return { ...worker, hasUnrecoveredWork: () => !recovered };
|
|
}
|