mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
[codex] Harden heartbeat runtime cleanup (#4233)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - The heartbeat runtime is the control-plane path that turns issue assignments into agent runs and recovers after process exits. > - Several edge cases could leave high-volume reads unbounded, stale runtime services visible, blocked dependency wakes too eager, or terminal adapter processes still around after output finished. > - These problems make operator views noisy and make long-running agent work less predictable. > - This pull request tightens the runtime/read paths and adds focused regression coverage. > - The benefit is safer heartbeat execution and cleaner runtime state without changing the public task model. ## What Changed - Bounded high-volume issue/log reads in runtime code paths. - Hardened heartbeat handling for blocked dependency wakes and terminal run cleanup. - Added adapter process cleanup coverage for terminal output cases. - Added workspace runtime control tests for stale command matching and stopped services. ## Verification - `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts server/src/__tests__/heartbeat-dependency-scheduling.test.ts ui/src/components/WorkspaceRuntimeControls.test.tsx` ## Risks - Medium risk because heartbeat cleanup and runtime filtering affect active agent execution paths. - No migrations. > Checked `ROADMAP.md`; this is runtime hardening and bug-fix work, not a new roadmap-level feature. ## Model Used - OpenAI Codex, GPT-5-based coding agent, tool-enabled repository editing and local test execution. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
73ef40e7be
commit
bcbbb41a4b
5 changed files with 80 additions and 5 deletions
|
|
@ -2041,6 +2041,7 @@ export function issueRoutes(
|
|||
),
|
||||
};
|
||||
}
|
||||
|
||||
const assigneeChanged =
|
||||
issue.assigneeAgentId !== existing.assigneeAgentId || issue.assigneeUserId !== existing.assigneeUserId;
|
||||
const statusChangedFromBacklog =
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ const MAX_INLINE_WAKE_COMMENT_BODY_TOTAL_CHARS = 12_000;
|
|||
const execFile = promisify(execFileCallback);
|
||||
const EXECUTION_PATH_HEARTBEAT_RUN_STATUSES = ["queued", "running", "scheduled_retry"] as const;
|
||||
const CANCELLABLE_HEARTBEAT_RUN_STATUSES = ["queued", "running", "scheduled_retry"] as const;
|
||||
const HEARTBEAT_RUN_TERMINAL_STATUSES = ["succeeded", "failed", "cancelled", "timed_out"] as const;
|
||||
const UNSUCCESSFUL_HEARTBEAT_RUN_TERMINAL_STATUSES = ["failed", "cancelled", "timed_out"] as const;
|
||||
export const BOUNDED_TRANSIENT_HEARTBEAT_RETRY_DELAYS_MS = [
|
||||
2 * 60 * 1000,
|
||||
|
|
@ -1661,6 +1662,14 @@ function isTrackedLocalChildProcessAdapter(adapterType: string) {
|
|||
return SESSIONED_LOCAL_ADAPTERS.has(adapterType);
|
||||
}
|
||||
|
||||
function isHeartbeatRunTerminalStatus(
|
||||
status: string | null | undefined,
|
||||
): status is (typeof HEARTBEAT_RUN_TERMINAL_STATUSES)[number] {
|
||||
return HEARTBEAT_RUN_TERMINAL_STATUSES.includes(
|
||||
status as (typeof HEARTBEAT_RUN_TERMINAL_STATUSES)[number],
|
||||
);
|
||||
}
|
||||
|
||||
// A positive liveness check means some process currently owns the PID.
|
||||
// On Linux, PIDs can be recycled, so this is a best-effort signal rather
|
||||
// than proof that the original child is still alive.
|
||||
|
|
@ -5536,8 +5545,8 @@ export function heartbeatService(db: Db) {
|
|||
|
||||
let outcome: "succeeded" | "failed" | "cancelled" | "timed_out";
|
||||
const latestRun = await getRun(run.id);
|
||||
if (latestRun?.status === "cancelled") {
|
||||
outcome = "cancelled";
|
||||
if (isHeartbeatRunTerminalStatus(latestRun?.status)) {
|
||||
outcome = latestRun.status;
|
||||
} else if (adapterResult.timedOut) {
|
||||
outcome = "timed_out";
|
||||
} else if ((adapterResult.exitCode ?? 0) === 0 && !adapterResult.errorMessage) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue