import { randomUUID } from "node:crypto"; import { eq, sql } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { agentSessionGoalActions, agentTaskSessions, agents, companies, createDb, heartbeatRuns, issues, } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "../__tests__/helpers/embedded-postgres.js"; import { applyRunnerGoalPrpEvent, isRunnerGoalActionCompleted, blockRunnerGoalRecovery, failRunnerGoalAction, RunnerGoalConflictError, runnerGoalService, } from "./runner-goals.js"; import { resolveExternalTestDatabaseUrl } from "../__tests__/helpers/external-test-database.js"; const external = resolveExternalTestDatabaseUrl("PAPERCLIP_RUNNER_GOALS_TEST_DATABASE_URL"); const embeddedPostgresSupport = external ? { supported: true } : await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; describeEmbeddedPostgres("runner goal service", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; beforeAll(async () => { if (!external) tempDb = await startEmbeddedPostgresTestDatabase("paperclip-runner-goals-"); db = createDb(external ?? tempDb!.connectionString); }, 20_000); afterEach(async () => { await db.delete(agentSessionGoalActions); await db.delete(agentTaskSessions); await db.delete(issues); await db.delete(heartbeatRuns); await db.delete(agents); await db.delete(companies); }); afterAll(async () => { await db?.$client.end(); await tempDb?.cleanup(); }); async function seed() { const companyId = randomUUID(); const agentId = randomUUID(); const issueId = randomUUID(); await db.insert(companies).values({ id: companyId, name: "Goal Test Company", issuePrefix: `G${companyId.replaceAll("-", "").slice(0, 6).toUpperCase()}`, requireBoardApprovalForNewAgents: false, }); await db.insert(agents).values({ id: agentId, companyId, name: "Codex Goal Agent", role: "engineer", status: "idle", adapterType: "paperclip_runner", adapterConfig: {}, runtimeConfig: {}, permissions: {}, }); await db.insert(issues).values({ id: issueId, companyId, identifier: `G-${Math.floor(Math.random() * 1_000_000)}`, title: "Pursue a durable goal", status: "in_progress", assigneeAgentId: agentId, }); return { companyId, agentId, issueId }; } it.each(["codex_local", "claude_local"])("does not convert %s into another execution path for goals", async (adapterType) => { const binding = await seed(); await db.update(agents).set({ adapterType, adapterConfig: { engine: "acp", mode: "persistent" } }); const enqueueOfflineControl = vi.fn(async () => {}); const service = runnerGoalService(db, { enqueueOfflineControl }); const projection = await service.projection(binding.companyId, binding.issueId); expect(projection?.capability).toMatchObject({ availability: "unsupported", reasonCode: "direct_adapter_goal_controller_unavailable", actions: [], }); await expect(service.act(binding.companyId, binding.issueId, { requestId: randomUUID(), agentId: binding.agentId, expectedRevision: 0, action: "create", objective: "Never start an ordinary prompt", })).rejects.toThrow(); expect(enqueueOfflineControl).not.toHaveBeenCalled(); expect(await db.select().from(agentSessionGoalActions)).toHaveLength(0); }); it("revisions failed goal starts and ignores repeated failure delivery", async () => { const binding = await seed(); const service = runnerGoalService(db, { dispatchLiveControl: () => ({ runId: "run-live", completion: Promise.resolve() }), queueLiveCommand: () => null, }); const requestId = randomUUID(); const accepted = await service.act(binding.companyId, binding.issueId, { requestId, agentId: binding.agentId, expectedRevision: 0, action: "create", objective: "Observe a failed start", }); const failed = await failRunnerGoalAction(db, { ...binding, adapterType: "paperclip_runner", }, requestId, "provider_start_failed"); expect(failed?.pendingAction).toBeNull(); expect(failed?.revision).toBe(accepted.projection.revision + 1); expect(await failRunnerGoalAction(db, { ...binding, adapterType: "paperclip_runner", }, requestId, "duplicate_failure")).toBeNull(); expect((await service.projection(binding.companyId, binding.issueId))?.revision).toBe(failed?.revision); }); it("enforces revisions, correlates acknowledgements, and fences cleared goals", async () => { const binding = await seed(); const service = runnerGoalService(db, { dispatchLiveControl: (_binding, control) => ({ runId: "run-live", completion: Promise.resolve().then(() => undefined), }), queueLiveCommand: () => null, }); const requestId = randomUUID(); const accepted = await service.act(binding.companyId, binding.issueId, { requestId, agentId: binding.agentId, expectedRevision: 0, action: "create", objective: "Finish the goal across turns", tokenBudget: 2_000, }); expect(accepted.status).toBe("accepted"); expect(accepted.projection.pendingAction).toBe("starting"); expect(await isRunnerGoalActionCompleted(db, binding, requestId)).toBe(false); const repeated = await service.act(binding.companyId, binding.issueId, { requestId, agentId: binding.agentId, expectedRevision: 0, action: "create", objective: "Finish the goal across turns", tokenBudget: 2_000, }); expect(repeated.status).toBe("pending"); await expect(service.act(binding.companyId, binding.issueId, { requestId, agentId: binding.agentId, expectedRevision: 0, action: "create", objective: "Reuse the key for different work", tokenBudget: 2_000, })).rejects.toMatchObject({ code: "idempotency_key_conflict", projection: { revision: 1 }, }); await expect(service.act(binding.companyId, binding.issueId, { requestId: randomUUID(), agentId: binding.agentId, expectedRevision: 0, action: "create", objective: "A stale change", })).rejects.toBeInstanceOf(RunnerGoalConflictError); await applyRunnerGoalPrpEvent(db, { ...binding, adapterType: "paperclip_runner", }, { eventType: "session.capabilities.updated", sourceSeq: 1, payload: { sessionGoals: { availability: "available", actions: ["set", "pause", "resume", "clear"], autonomousUpdates: true, persistentAcrossResume: true, maxObjectiveChars: 4_000, tokenBudgetControl: true, usageReporting: true, }, }, }); const updated = await applyRunnerGoalPrpEvent(db, { ...binding, adapterType: "paperclip_runner", }, { eventType: "session.goal.updated", sourceSeq: 2, payload: { requestId, workingNow: true, goal: { objective: "Finish the goal across turns", status: "active", tokenBudget: 2_000, tokensUsed: 25, elapsedSeconds: 3, iterations: 1, createdAt: "2026-08-28T12:00:00.000Z", updatedAt: "2026-08-28T12:00:03.000Z", completedAt: null, }, }, }); expect(updated).toMatchObject({ goal: { status: "active", workingNow: true }, pendingAction: null, }); expect(await isRunnerGoalActionCompleted(db, binding, requestId)).toBe(true); expect(await isRunnerGoalActionCompleted(db, { ...binding, issueId: randomUUID() }, requestId)).toBe(false); expect(await isRunnerGoalActionCompleted(db, { ...binding, agentId: randomUUID() }, requestId)).toBe(false); expect(await isRunnerGoalActionCompleted(db, { ...binding, companyId: randomUUID() }, requestId)).toBe(false); const failedClear = await applyRunnerGoalPrpEvent(db, { ...binding, adapterType: "paperclip_runner", }, { eventType: "session.goal.cleared", sourceSeq: 3, payload: { requestId: randomUUID(), error: "provider temporarily rejected clear", goal: null, sessionGoals: { availability: "available", actions: ["set", "pause", "resume", "clear"], autonomousUpdates: true, persistentAcrossResume: true, maxObjectiveChars: 4_000, tokenBudgetControl: true, usageReporting: true, }, }, }); expect(failedClear).toBeNull(); const afterFailedClear = await service.projection( binding.companyId, binding.issueId, binding.agentId, ); expect(afterFailedClear).toMatchObject({ goal: { objective: "Finish the goal across turns", status: "active" }, }); const clearRequestId = randomUUID(); const clearAccepted = await service.act(binding.companyId, binding.issueId, { requestId: clearRequestId, agentId: binding.agentId, expectedRevision: afterFailedClear!.revision, action: "clear", }); expect(clearAccepted.projection.pendingAction).toBe("clearing"); const cleared = await applyRunnerGoalPrpEvent(db, { ...binding, adapterType: "paperclip_runner", }, { eventType: "session.goal.cleared", sourceSeq: 4, payload: { requestId: clearRequestId, goal: null, workingNow: false }, }); expect(cleared).toMatchObject({ goal: null, pendingAction: null }); const stale = await applyRunnerGoalPrpEvent(db, { ...binding, adapterType: "paperclip_runner", }, { eventType: "session.goal.updated", sourceSeq: 3, payload: { goal: { objective: "Stale resurrection", status: "active" }, }, }); expect(stale).toBeNull(); await expect(service.projection(binding.companyId, binding.issueId, binding.agentId)) .resolves.toMatchObject({ goal: null }); }); it("accepts a reset source sequence from a successor run of the same durable runner", async () => { const binding = await seed(); const runnerId = randomUUID(); const nativeSessionId = randomUUID(); const priorRunId = randomUUID(); const nextRunId = randomUUID(); await db.insert(heartbeatRuns).values([ { id: priorRunId, companyId: binding.companyId, agentId: binding.agentId, nativeIssueId: binding.issueId, status: "succeeded", invocationSource: "on_demand", runnerInstanceId: runnerId, nativeSessionId }, { id: nextRunId, companyId: binding.companyId, agentId: binding.agentId, nativeIssueId: binding.issueId, status: "running", invocationSource: "on_demand", runnerInstanceId: runnerId, nativeSessionId }, ]); const first = await applyRunnerGoalPrpEvent(db, { ...binding, adapterType: "paperclip_runner", }, { eventType: "session.goal.updated", sourceInstanceId: runnerId, sourceRunId: priorRunId, sourceSeq: 9, payload: { goal: { objective: "First heartbeat objective", status: "complete", }, }, }); expect(first).toMatchObject({ goal: { status: "complete" } }); await expect(applyRunnerGoalPrpEvent(db, { ...binding, adapterType: "paperclip_runner", }, { eventType: "session.goal.snapshot", sourceInstanceId: runnerId, sourceRunId: priorRunId, sourceSeq: 10, payload: { goal: null }, })).resolves.toBeNull(); await expect(runnerGoalService(db).projection( binding.companyId, binding.issueId, binding.agentId, )).resolves.toMatchObject({ revision: 1, goal: { status: "complete" } }); const successorEvent = { eventType: "session.goal.updated", sourceInstanceId: runnerId, sourceRunId: nextRunId, sourceSeq: 1, payload: { goal: { objective: "Successor heartbeat objective", status: "active", }, }, }; const eventBinding = { ...binding, adapterType: "paperclip_runner" }; // Merely changing the source namespace is not proof of succession. for (const invalidOwner of [ { nativeSessionId: randomUUID() }, { nativeIssueId: randomUUID() }, { runnerInstanceId: randomUUID() }, { status: "succeeded" }, ]) { // Corrupt a historical binding to verify the reader's independent guard. await db.transaction(async (tx) => { await tx.execute(sql`set local session_replication_role = replica`); await tx.update(heartbeatRuns).set(invalidOwner).where(eq(heartbeatRuns.id, nextRunId)); }); await expect(applyRunnerGoalPrpEvent(db, eventBinding, successorEvent)).resolves.toBeNull(); await db.transaction(async (tx) => { await tx.execute(sql`set local session_replication_role = replica`); await tx.update(heartbeatRuns).set({ nativeSessionId, nativeIssueId: binding.issueId, runnerInstanceId: runnerId, status: "running" }) .where(eq(heartbeatRuns.id, nextRunId)); }); } await db.update(heartbeatRuns).set({ status: "running" }).where(eq(heartbeatRuns.id, priorRunId)); await expect(applyRunnerGoalPrpEvent(db, eventBinding, successorEvent)).resolves.toBeNull(); await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, priorRunId)); const successor = await applyRunnerGoalPrpEvent(db, eventBinding, successorEvent); expect(successor).toMatchObject({ goal: { objective: "Successor heartbeat objective", status: "active" }, }); const duplicate = await applyRunnerGoalPrpEvent(db, { ...binding, adapterType: "paperclip_runner", }, { eventType: "session.goal.cleared", sourceInstanceId: runnerId, sourceRunId: nextRunId, sourceSeq: 1, payload: { goal: null }, }); expect(duplicate).toBeNull(); const delayed = (eventType: string, sourceSeq: number) => ({ eventType, sourceInstanceId: runnerId, sourceRunId: priorRunId, sourceSeq, payload: { goal: { objective: "Stale predecessor", status: "active" } }, }); // The predecessor cannot erase the successor's active goal. await expect(applyRunnerGoalPrpEvent(db, eventBinding, delayed("session.goal.cleared", 100))).resolves.toBeNull(); await expect(runnerGoalService(db).projection(binding.companyId, binding.issueId)).resolves.toMatchObject({ revision: 2, goal: { objective: "Successor heartbeat objective" }, }); await applyRunnerGoalPrpEvent(db, eventBinding, { eventType: "session.goal.cleared", sourceInstanceId: runnerId, sourceRunId: nextRunId, sourceSeq: 2, payload: { goal: null }, }); // Nor can it resurrect the cleared goal, even once the successor settles. await db.update(heartbeatRuns).set({ status: "succeeded" }); await expect(applyRunnerGoalPrpEvent(db, eventBinding, delayed("session.goal.updated", 101))).resolves.toBeNull(); await expect(applyRunnerGoalPrpEvent(db, eventBinding, { eventType: "session.goal.updated", sourceSeq: 102, payload: delayed("", 0).payload, })).resolves.toBeNull(); await expect(runnerGoalService(db).projection(binding.companyId, binding.issueId)).resolves.toMatchObject({ revision: 3, goal: null }); const [session] = await db.select().from(agentTaskSessions); expect(session).toMatchObject({ goalSourceId: `${runnerId}:${nextRunId}`, goalSourceCursor: 2 }); }); it.each(["current", "current_complete", "not_current", "reassigned", "blocked", "prior_running", "next_finished", "foreign_runner", "missing_session", "unfinished_goal", "unreadable_goal", "conversation_reset"] as const)("fences a fresh goal source by current execution authority (%s)", async kind => { const binding = await seed(), priorRunId = randomUUID(), nextRunId = randomUUID(); const priorRunnerId = randomUUID(), nextRunnerId = randomUUID(); const priorSessionId = randomUUID(), nextSessionId = randomUUID(); const eventBinding = { ...binding, adapterType: "paperclip_runner" }; await db.insert(heartbeatRuns).values([ { id: priorRunId, ...binding, nativeIssueId: binding.issueId, runtimeMode: "native", status: "failed", runnerInstanceId: priorRunnerId, nativeSessionId: priorSessionId }, { id: nextRunId, ...binding, nativeIssueId: binding.issueId, runtimeMode: "native", status: "running", runnerInstanceId: nextRunnerId, nativeSessionId: nextSessionId }, ]); await db.update(issues).set({ executionRunId: nextRunId }).where(eq(issues.id, binding.issueId)); const priorEvent = { eventType: "session.goal.updated", sourceInstanceId: priorRunnerId, sourceRunId: priorRunId, sourceSeq: 20, payload: { goal: { objective: "Retained goal", status: kind === "current_complete" ? "complete" : "active" } } }; await applyRunnerGoalPrpEvent(db, eventBinding, priorEvent); if (kind !== "unfinished_goal" && kind !== "current_complete") await applyRunnerGoalPrpEvent(db, eventBinding, { ...priorEvent, eventType: "session.goal.cleared", sourceSeq: 21, payload: { goal: null }, }); if (kind === "not_current") await db.update(issues).set({ executionRunId: null }).where(eq(issues.id, binding.issueId)); if (kind === "reassigned") await db.update(issues).set({ assigneeAgentId: null }).where(eq(issues.id, binding.issueId)); if (kind === "blocked") await db.update(issues).set({ status: "blocked" }).where(eq(issues.id, binding.issueId)); if (kind === "prior_running") await db.update(heartbeatRuns).set({ status: "running" }).where(eq(heartbeatRuns.id, priorRunId)); if (kind === "next_finished") await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, nextRunId)); if (kind === "foreign_runner") await db.update(heartbeatRuns).set({ runnerInstanceId: randomUUID() }).where(eq(heartbeatRuns.id, nextRunId)); if (kind === "missing_session") await db.update(heartbeatRuns).set({ nativeSessionId: null }).where(eq(heartbeatRuns.id, nextRunId)); if (kind === "unreadable_goal") await db.update(agentTaskSessions).set({ goalJson: { legacy: "unknown" } }).where(eq(agentTaskSessions.taskKey, binding.issueId)); if (kind === "conversation_reset") await db.update(issues).set({ conversationAgentId: binding.agentId, conversationUserId: "goal-reset-user", conversationState: "active", conversationSessionGeneration: 2, }).where(eq(issues.id, binding.issueId)); const nextEvent = { eventType: "session.goal.updated", sourceInstanceId: nextRunnerId, sourceRunId: nextRunId, sourceSeq: 1, payload: { goal: { objective: "Current fresh session", status: "active" } } }; const changed = await applyRunnerGoalPrpEvent(db, eventBinding, nextEvent); const [session] = await db.select().from(agentTaskSessions).where(eq(agentTaskSessions.taskKey, binding.issueId)); if (kind !== "current" && kind !== "current_complete") { expect(changed).toBeNull(); expect(session.goalSourceId).toBe(`${priorRunnerId}:${priorRunId}`); expect(session.goalSourceCursor).toBe(kind === "unfinished_goal" ? 20 : 21); return; } expect(changed?.goal).toMatchObject({ objective: "Current fresh session", status: "active" }); expect(session).toMatchObject({ goalSourceId: `${nextRunnerId}:${nextRunId}`, goalSourceCursor: 1 }); await applyRunnerGoalPrpEvent(db, eventBinding, { ...nextEvent, eventType: "session.goal.cleared", sourceSeq: 2, payload: { goal: null } }); await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, nextRunId)); for (const stale of [{ ...priorEvent, sourceSeq: 1_000 }, nextEvent, { eventType: "session.goal.updated", sourceSeq: 2_000, payload: priorEvent.payload }]) { expect(await applyRunnerGoalPrpEvent(db, eventBinding, stale)).toBeNull(); } expect(await runnerGoalService(db).projection(binding.companyId, binding.issueId)).toMatchObject({ goal: null }); expect(await db.select().from(agentSessionGoalActions)).toHaveLength(0); }); it("keeps a missing resumed goal blocked across restored empty snapshots", async () => { const binding = await seed(); const eventBinding = { ...binding, adapterType: "paperclip_runner" }; await applyRunnerGoalPrpEvent(db, eventBinding, { eventType: "session.goal.updated", sourceSeq: 1, payload: { goal: { objective: "Recover the durable provider goal", status: "active", }, }, }); const blocked = await applyRunnerGoalPrpEvent(db, eventBinding, { eventType: "session.goal.snapshot", sourceSeq: 2, payload: { goal: null }, }); expect(blocked).toMatchObject({ revision: 2, goal: { objective: "Recover the durable provider goal", status: "blocked", lastReason: "provider_session_goal_missing_after_resume", }, }); await expect(applyRunnerGoalPrpEvent(db, eventBinding, { eventType: "session.goal.snapshot", sourceSeq: 3, payload: { goal: null }, })).resolves.toBeNull(); const [session] = await db.select({ revision: agentTaskSessions.goalRevision, sourceCursor: agentTaskSessions.goalSourceCursor, desiredState: agentTaskSessions.goalDesiredState, goal: agentTaskSessions.goalJson, }).from(agentTaskSessions); expect(session).toMatchObject({ revision: 2, sourceCursor: 3, desiredState: "paused", goal: { objective: "Recover the durable provider goal", status: "blocked", lastReason: "provider_session_goal_missing_after_resume", }, }); }); it("projects committed capability and goal events exactly once across duplicate delivery", async () => { const binding = await seed(); const eventBinding = { ...binding, adapterType: "paperclip_runner", }; const source = { sourceInstanceId: "native-runner", sourceRunId: "goal-heartbeat", }; const capabilityEvent = { ...source, eventType: "session.capabilities.updated", sourceSeq: 1, payload: { sessionGoals: { availability: "available", actions: ["set", "pause", "resume", "clear"], autonomousUpdates: true, persistentAcrossResume: true, maxObjectiveChars: 4_000, tokenBudgetControl: true, usageReporting: true, }, }, }; const goalEvent = { ...source, eventType: "session.goal.updated", sourceSeq: 2, payload: { workingNow: true, goal: { objective: "Project the durable provider goal", status: "active", tokenBudget: 4_000, tokensUsed: 50, elapsedSeconds: 2, iterations: 1, }, }, }; const capability = await applyRunnerGoalPrpEvent(db, eventBinding, capabilityEvent); expect(capability).toMatchObject({ capability: { availability: "available", verified: true }, revision: 1, }); await expect( applyRunnerGoalPrpEvent(db, eventBinding, capabilityEvent), ).resolves.toBeNull(); const goal = await applyRunnerGoalPrpEvent(db, eventBinding, goalEvent); expect(goal).toMatchObject({ goal: { objective: "Project the durable provider goal", status: "active", workingNow: true, }, revision: 2, }); await expect( applyRunnerGoalPrpEvent(db, eventBinding, goalEvent), ).resolves.toBeNull(); await expect( applyRunnerGoalPrpEvent(db, eventBinding, { ...goalEvent, sourceSeq: 3, }), ).resolves.toBeNull(); const [session] = await db.select({ revision: agentTaskSessions.goalRevision, sourceCursor: agentTaskSessions.goalSourceCursor, capability: agentTaskSessions.goalCapabilityJson, goal: agentTaskSessions.goalJson, }).from(agentTaskSessions); expect(session).toMatchObject({ revision: 2, sourceCursor: 3, capability: { availability: "available" }, goal: { objective: "Project the durable provider goal", status: "active", workingNow: true, }, }); }); it("blocks an unrecoverable active goal with a stable resumable reason", async () => { const binding = await seed(); await applyRunnerGoalPrpEvent(db, { ...binding, adapterType: "paperclip_runner" }, { eventType: "session.goal.updated", sourceSeq: 1, payload: { goal: { objective: "Recover this goal after restart", status: "active", tokensUsed: 10, elapsedSeconds: 5, iterations: 1, }, }, }); const blocked = await blockRunnerGoalRecovery(db, { ...binding, adapterType: "paperclip_runner", }); expect(blocked).toMatchObject({ goal: { status: "blocked", workingNow: false, lastReason: "provider_session_goal_recovery_failed", }, }); const [session] = await db.select({ desired: agentTaskSessions.goalDesiredState, }).from(agentTaskSessions); expect(session?.desired).toBe("paused"); }); it("routes quiescent goal controls through durable recovery instead of a stale live authority", async () => { const binding = await seed(); let liveDispatches = 0; let queuedCommands = 0; const offlineControls: string[] = []; const service = runnerGoalService(db, { dispatchLiveControl: () => { liveDispatches += 1; return { runId: "stale-run", completion: Promise.resolve(), }; }, queueLiveCommand: () => { queuedCommands += 1; return null; }, enqueueOfflineControl: async ({ requestId }) => { offlineControls.push(requestId); }, }); const requestId = randomUUID(); const accepted = await service.act(binding.companyId, binding.issueId, { requestId, agentId: binding.agentId, expectedRevision: 0, action: "create", objective: "Resume this goal through a durable heartbeat", }); expect(accepted.status).toBe("accepted"); expect(liveDispatches).toBe(0); expect(queuedCommands).toBe(0); expect(offlineControls).toEqual([requestId]); await expect( db .select({ status: agentSessionGoalActions.status }) .from(agentSessionGoalActions), ).resolves.toEqual([{ status: "pending" }]); }); });