mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 02:20:38 +09:00
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - The heartbeat runtime, agent import path, and agent configuration defaults determine whether work is dispatched safely and predictably. > - Several accumulated fixes all touched agent execution recovery, wake routing, import behavior, and runtime concurrency defaults. > - Those changes need to land together so the heartbeat service and agent creation defaults stay internally consistent. > - This pull request groups the runtime/governance changes from the split branch into one standalone branch. > - The benefit is safer recovery for stranded runs, bounded high-volume reads, imported-agent approval correctness, skill-template support, and a clearer default concurrency policy. ## What Changed - Fixed stranded continuation recovery so successful automatic retries are requeued instead of incorrectly blocking the issue. - Bounded high-volume issue/log reads across issue, heartbeat, agent, project, and workspace paths. - Fixed imported-agent approval and instruction-path permission handling. - Quarantined seeded worktree execution state during worktree provisioning. - Queued approval follow-up wakes and hardened SQL_ASCII heartbeat output handling. - Added reusable agent instruction templates for hiring flows. - Set the default max concurrent agent runs to five and updated related UI/tests/docs. ## Verification - `pnpm install --frozen-lockfile` - `pnpm exec vitest run server/src/__tests__/company-portability.test.ts server/src/__tests__/heartbeat-process-recovery.test.ts server/src/__tests__/heartbeat-comment-wake-batching.test.ts server/src/__tests__/heartbeat-list.test.ts server/src/__tests__/issues-service.test.ts server/src/__tests__/agent-permissions-routes.test.ts packages/adapter-utils/src/server-utils.test.ts ui/src/lib/new-agent-runtime-config.test.ts` - Split integration check: merged this branch first, followed by the other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge conflicts. - Confirmed this branch does not include `pnpm-lock.yaml`. ## Risks - Medium risk: touches heartbeat recovery, queueing, and issue list bounds in central runtime paths. - Imported-agent and concurrency default behavior changes may affect existing automation that assumes one-at-a-time default runs. - No database migrations are included. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic code-editing/runtime with local shell and GitHub CLI access; exact context window and reasoning mode are not exposed by the Paperclip harness. ## 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
057fee4836
commit
16b2b84d84
38 changed files with 1569 additions and 240 deletions
|
|
@ -3,12 +3,15 @@ import os from "node:os";
|
|||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
agents,
|
||||
authUsers,
|
||||
companies,
|
||||
createDb,
|
||||
issueComments,
|
||||
issues,
|
||||
projects,
|
||||
routines,
|
||||
routineTriggers,
|
||||
|
|
@ -17,6 +20,7 @@ import {
|
|||
copyGitHooksToWorktreeGitDir,
|
||||
copySeededSecretsKey,
|
||||
pauseSeededScheduledRoutines,
|
||||
quarantineSeededWorktreeExecutionState,
|
||||
readSourceAttachmentBody,
|
||||
rebindWorkspaceCwd,
|
||||
resolveSourceConfigPath,
|
||||
|
|
@ -282,6 +286,138 @@ describe("worktree helpers", () => {
|
|||
expect(full.nullifyColumns).toEqual({});
|
||||
});
|
||||
|
||||
itEmbeddedPostgres("quarantines copied live execution state in seeded worktree databases", async () => {
|
||||
const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-quarantine-");
|
||||
const db = createDb(tempDb.connectionString);
|
||||
const companyId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
const idleAgentId = randomUUID();
|
||||
const inProgressIssueId = randomUUID();
|
||||
const todoIssueId = randomUUID();
|
||||
const reviewIssueId = randomUUID();
|
||||
const userIssueId = randomUUID();
|
||||
|
||||
try {
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: "WTQ",
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(agents).values([
|
||||
{
|
||||
id: agentId,
|
||||
companyId,
|
||||
name: "CodexCoder",
|
||||
role: "engineer",
|
||||
status: "running",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {
|
||||
heartbeat: { enabled: true, intervalSec: 60 },
|
||||
wakeOnDemand: true,
|
||||
},
|
||||
permissions: {},
|
||||
},
|
||||
{
|
||||
id: idleAgentId,
|
||||
companyId,
|
||||
name: "Reviewer",
|
||||
role: "reviewer",
|
||||
status: "idle",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: { heartbeat: { enabled: false, intervalSec: 300 } },
|
||||
permissions: {},
|
||||
},
|
||||
]);
|
||||
await db.insert(issues).values([
|
||||
{
|
||||
id: inProgressIssueId,
|
||||
companyId,
|
||||
title: "Copied in-flight issue",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
assigneeAgentId: agentId,
|
||||
issueNumber: 1,
|
||||
identifier: "WTQ-1",
|
||||
executionAgentNameKey: "codexcoder",
|
||||
executionLockedAt: new Date("2026-04-18T00:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: todoIssueId,
|
||||
companyId,
|
||||
title: "Copied assigned todo issue",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
assigneeAgentId: agentId,
|
||||
issueNumber: 2,
|
||||
identifier: "WTQ-2",
|
||||
},
|
||||
{
|
||||
id: reviewIssueId,
|
||||
companyId,
|
||||
title: "Copied assigned review issue",
|
||||
status: "in_review",
|
||||
priority: "medium",
|
||||
assigneeAgentId: idleAgentId,
|
||||
issueNumber: 3,
|
||||
identifier: "WTQ-3",
|
||||
},
|
||||
{
|
||||
id: userIssueId,
|
||||
companyId,
|
||||
title: "Copied user issue",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
assigneeUserId: "user-1",
|
||||
issueNumber: 4,
|
||||
identifier: "WTQ-4",
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(quarantineSeededWorktreeExecutionState(tempDb.connectionString)).resolves.toEqual({
|
||||
disabledTimerHeartbeats: 1,
|
||||
resetRunningAgents: 1,
|
||||
quarantinedInProgressIssues: 1,
|
||||
unassignedTodoIssues: 1,
|
||||
unassignedReviewIssues: 1,
|
||||
});
|
||||
|
||||
const [quarantinedAgent] = await db.select().from(agents).where(eq(agents.id, agentId));
|
||||
expect(quarantinedAgent?.status).toBe("idle");
|
||||
expect(quarantinedAgent?.runtimeConfig).toMatchObject({
|
||||
heartbeat: { enabled: false, intervalSec: 60 },
|
||||
wakeOnDemand: true,
|
||||
});
|
||||
|
||||
const [inProgressIssue] = await db.select().from(issues).where(eq(issues.id, inProgressIssueId));
|
||||
expect(inProgressIssue?.status).toBe("blocked");
|
||||
expect(inProgressIssue?.assigneeAgentId).toBeNull();
|
||||
expect(inProgressIssue?.executionAgentNameKey).toBeNull();
|
||||
expect(inProgressIssue?.executionLockedAt).toBeNull();
|
||||
|
||||
const [todoIssue] = await db.select().from(issues).where(eq(issues.id, todoIssueId));
|
||||
expect(todoIssue?.status).toBe("todo");
|
||||
expect(todoIssue?.assigneeAgentId).toBeNull();
|
||||
|
||||
const [reviewIssue] = await db.select().from(issues).where(eq(issues.id, reviewIssueId));
|
||||
expect(reviewIssue?.status).toBe("in_review");
|
||||
expect(reviewIssue?.assigneeAgentId).toBeNull();
|
||||
|
||||
const [userIssue] = await db.select().from(issues).where(eq(issues.id, userIssueId));
|
||||
expect(userIssue?.status).toBe("todo");
|
||||
expect(userIssue?.assigneeUserId).toBe("user-1");
|
||||
|
||||
const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, inProgressIssueId));
|
||||
expect(comments).toHaveLength(1);
|
||||
expect(comments[0]?.body).toContain("Quarantined during worktree seed");
|
||||
} finally {
|
||||
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
|
||||
await tempDb.cleanup();
|
||||
}
|
||||
}, 20_000);
|
||||
|
||||
it("copies the source local_encrypted secrets key into the seeded worktree instance", () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
|
||||
const originalInlineMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue