Files
PaperClipAI/server/src/services/native-runtime/agent-chat-response-recovery.test.ts
T
DottaandPaperclip 53f480d1b3 fix(native): preserve warm continuity and deferred review replies (#18631)
## Thinking Path

> - Paperclip manages AI agents and their saved work.
> - Product E2E tests must check saved personal files across separate
turns.
> - The server records file collection in the durable `instruction_save`
run-log event.
> - The warm test read optional terminal result metadata instead and
rejected a recorded successful save.
> - This pull request reads the complete event stream for the current
run and uses its latest save receipt.
> - The prompt now specifies the exact nine bytes required by the
temporary-file transfer count.
> - Human review actions select the actual interaction card, which can
share its interaction ID with a navigation button.
> - A later real three-turn run exposed a missing reply after workspace
finalization moved to its durable recovery owner.
> - Reply recovery handled completed tasks but omitted ordinary tasks
that awaited review.
> - This pull request also publishes their recorded final reply once
while preserving the pending review.

## Linked Issues or Issue Description

**What happened?**

The [retained live continuity
attempt](https://github.com/paperclipai/paperclip/actions/runs/41477302876/job/125420001074)
saved the correct memory file and unchanged 8 MiB binary. Its durable
run log contained a successful `instruction_save` receipt. The oracle
read `resultJson.instructionSave`, which was absent, and failed before
turn two. The temporary file also had a trailing LF. Its prompt allowed
that result, while the byte-count check required nine bytes.

**Expected behavior**

The oracle must use the documented authoritative save event. A missing
receipt, a later failed save, incorrect transfer counts, or another
run's events must fail. The prompt must specify the bytes that its
independent check requires.

**Steps to reproduce**

1. Run `daytona-warm-continuity.runner-codex.daytona.warm-three-turn`
with the configured protected credentials and immutable image.
2. Complete a native turn whose terminal result has no `instructionSave`
field.
3. Compare the oracle output with the same run's successful
`instruction_save` event.

A subsequent [three-turn
attempt](https://github.com/paperclipai/paperclip/actions/runs/41495337334/job/125451304553)
passed every personal-file assertion and reused the same warm process.
Its first final reply was recorded as a completed agent-message event
but was absent from comments. The log shows
`native_workspace_finalization_busy`; the durable recovery path excluded
`needs_review` replies. The final message matcher correctly failed. This
failure remains recorded.

The observed application revision was
`aab87ce70744660a4a21c77278b0ede267c04720`. Related work: #14695 and
#18494. The retained failed result keeps its original grade and
definition.

## What Changed

- Read all run-event pages through the existing bounded collector.
- Require events to belong to the requested run and select the latest
`instruction_save` event.
- Retain strict byte counts and reject missing, failed, pending or
superseded receipts.
- Specify nine ASCII bytes and no trailing newline for the temporary
file.
- Scope human review actions to the interaction card and its exact
interaction ID.
- Advance the warm suite definition to version 5 and document its
evidence source.
- Recover recorded final replies for ordinary native tasks with an
accepted `needs_review` result and terminal state.
- Preserve pending reviews, existing reply selection,
company/turn/digest checks and external governed-response authorization.
- Update the task feature and agent access maps.

## Verification

- Real PostgreSQL regression: deferred review reply discovery failed
before the production repair (expected one candidate; found zero). The
final-message event and pending review were present.
- Recovery cases cover one publication, no repeated provider run,
unchanged review, and rejection of absent/wrong-turn final messages, bad
digest and suppressed comments.

- Red: seven new boundary regressions failed against the old receipt
lookup.
- Green: 169 focused oracle, event pagination and catalog checks passed.
Product E2E typecheck passed after rebuilding local package
declarations.
- Retained-evidence replay confirms that the new lookup finds the real
successful save event. The original attempt remains failed because its
temporary file included an LF.
- Recursive typecheck passed before the final recovery change; server
typecheck passed afterward. The recovery and control-plane suites passed
all 112 tests. Full build and map checks passed.
- The broad local test run reached one unrelated 30-second timeout in
`attention-retention-work.test.ts` (maintained-review/confirmation
batching). The same unchanged case then passed on one targeted retry
(39.9 seconds including module load). The original failure remains
retained. The broad run also reported one unchanged workspace-conflict
case, which passed on its targeted retry (14.85 seconds including module
load). Both original failures remain retained. The redundant broad local
run was stopped after current-head CI completed all required gates: 55
successful checks and 3 selection skips. No all-green local broad-run
claim is made.
- The separate artifact-transfer cell already passed on the recorded
application revision: independent source and delivered bytes matched at
407,485 bytes, with a completed task, succeeded native run and cleared
execution locks. This does not establish a three-turn continuity pass.

- [Real native three-turn
acceptance](https://github.com/paperclipai/paperclip/actions/runs/41499102994/job/125465795210)
**passed** at `3b207a42943e9eeb371d4f2d5623de72093eb6d5`, suite
definition 5. Turn one exercised `committed_task_response_recovered`.
All three completed final replies were persisted once and in order. All
three strict personal-file receipts passed. The unchanged 8 MiB binary
survived; later transfers copied only 36 and 54 bytes. The lease was
created, resumed, and resumed with the same runner PID/start time,
runner instance, native session and provider session. Final task was
`done`, all three runs succeeded, and checkout/execution locks were
null.
- The aggregate startup summary has no separate provider-process PID
proof. The raw run records independently prove the stable runner process
and provider session stated above. This single native cell does not
qualify the legacy cell or a full provider matrix.
- The passing campaign reused the signed immutable runner image. It
required no disk expansion. Earlier campaign image builds used the
recorded temporary 120 GiB mitigation; default 60 GiB build capacity
remains unqualified. All scoped completed probes were removed and
independently returned 404; org CPU/disk usage returned to zero. This
does not resolve unknown historical resources under a different
credential identity.
- Current-head Greptile is 5/5; review-thread pagination is complete
with zero threads. All 55 current-head checks are successful; 3
selection checks are skipped. The paid campaign, result enforcement and
report publication all completed successfully.

## Risks

- Publishing a reply must not approve the work. Recovery only copies the
accepted turn’s completed final message. It preserves the pending review
and does not start an executor.
- Conversation and external governed-response paths retain their
existing separate authorization.

The latest save event can record failure after an earlier success. The
oracle rejects that case. Event pagination, missing receipts and
cross-run evidence also fail closed. The exact temporary-file byte count
remains required. The review selector still requires the intended
interaction ID and enabled action on its actual card. The production
change is limited to ordinary internal-task reply recovery. It still
requires an accepted result, a matching successful terminal disposition,
the completed final message, and existing publication authority.

## Model Used

OpenAI Codex, based on GPT-6, with shell and code tools. The exact
deployment variant and context-window size are 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 `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [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
- [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-11-02 14:19:48 -06:00

690 lines
47 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { and, eq, sql } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { agents, authUsers, companies, completionContracts, createDb, heartbeatRunEvents, heartbeatRuns, issueComments, issues,
nativeRunFinalizations, nativeRunResults, issueThreadInteractions, chatTaskHandoffs, chatCompletionDeliveries, agentWakeupRequests, type Db } from "@paperclipai/db";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
import { resolveExternalTestDatabaseUrl } from "../../__tests__/helpers/external-test-database.js";
import { NativeRunCoordinatorStore } from "./native-run-coordinator-store.js";
import { finalizeNativeRun, recoverCommittedNativeChatResponse } from "./native-run-finalizer.js";
import { nativeResponsePresentationWork } from "./native-response-presentation-work.js";
import { createDeliveryWorkCoordinator } from "../delivery-work-coordinator.js";
import { idleWorkSnapshot } from "../task-admission.js";
import { chatCompletionDeliveryService, existingChatCompletionReply } from "../chat-completion-delivery.js";
import { instanceSettingsService } from "../instance-settings.js";
import { issueService } from "../issues.js";
import * as nativeResponseNotifications from "../native-response-presentation-notifications.js";
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_AGENT_CHAT_RESPONSE_TEST_DATABASE_URL");
const support = external ? { supported: true } : await getEmbeddedPostgresTestSupport();
describe.skipIf(!support.supported)("committed Agent Chat reply recovery", () => {
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
let db: Db;
beforeAll(async () => {
if (!external) database = await startEmbeddedPostgresTestDatabase("agent-chat-response-");
db = createDb(external ?? database!.connectionString);
}, 60_000);
afterAll(async () => { await db?.$client.end(); await database?.cleanup(); });
async function fixture(conversation = true, options: { succeed?: boolean; disposition?: "done" | "needs_review" } = {}) {
const companyId = randomUUID(), agentId = randomUUID(), issueId = randomUUID(), runId = randomUUID();
const sessionId = randomUUID(), runnerId = randomUUID(), userId = `fixture-${randomUUID()}`, wakeId = randomUUID();
await db.insert(authUsers).values({ id: userId, name: "Conversation owner", email: `${userId}@example.test`,
createdAt: new Date("2020-01-01T00:00:00Z"), updatedAt: new Date("2020-01-01T00:00:00Z") });
await db.insert(companies).values({ id: companyId, name: "Reply recovery fixture", issuePrefix: companyId.slice(0, 8) });
await db.insert(agents).values({ id: agentId, companyId, name: "Conversation agent", adapterType: "paperclip_runner" });
await db.insert(issues).values({ id: issueId, companyId, title: "Conversation reply", status: "in_progress", assigneeAgentId: agentId,
...(conversation ? { conversationAgentId: agentId, conversationUserId: userId, conversationState: "active", conversationSessionGeneration: 0 } : {}) });
await db.insert(issueComments).values({ id: wakeId, companyId, issueId, authorUserId: userId, body: "Hire one teammate and tell me when ready.",
createdAt: new Date("2020-01-01T00:00:00Z") });
const [contract] = await db.insert(completionContracts).values({ companyId, issueId, revision: 1,
schemaVersion: "paperclip.completion-contract.v1", policyVersion: "test", risk: "standard", completionAuthority: "server_arbiter",
incompleteCriteriaPolicy: "preserve_non_terminal", contractJson: { revision: "test", objective: "Answer the user", criteria: [] },
canonicalSha256: "fixture-conversation-contract", createdByActorType: "system", createdByActorId: "test" }).returning();
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, nativeIssueId: issueId, nativeSessionId: sessionId,
runnerInstanceId: runnerId, completionContractId: contract.id, completionContractSha256: contract.canonicalSha256,
status: "running", runtimeMode: "native", responsibleUserId: userId,
contextSnapshot: { issueId, source: "issue.comment", wakeCommentId: wakeId, ...(conversation ? { conversationSessionGeneration: 0 } : {}) } });
await db.update(issues).set({ executionRunId: runId }).where(eq(issues.id, issueId));
const port = new NativeRunCoordinatorStore(db, { companyId, issueId, runId, agentId, normalizedSessionId: sessionId,
runnerSourceInstanceId: runnerId, completionContractId: contract.id, completionContractSha256: contract.canonicalSha256,
completionContractRevision: "test", completionContractCriterionIds: [] });
const reply = "The teammate is hired and ready for your next instruction.";
const prpEvent = { runId, normalizedSessionId: sessionId, turnId: "hire-turn", sourceEventId: "fixture-final-answer",
payload: { kind: "agentMessage", channel: "final", providerPhase: "final", text: reply } };
await db.insert(heartbeatRunEvents).values({ companyId, runId, agentId, seq: 170, eventType: "item.completed", payload: { prpEvent } });
const disposition = options.disposition ?? "done";
await port.completeRun({ result: { schema: "paperclip.run_result.v1", reportedWorkDisposition: disposition,
summary: "A completed hire, with its final message preserved separately.", completionClaim: { contractRevision: "test",
objectiveSatisfied: true, criteria: [], remainingWork: [] }, evidence: [], verification: [],
attentionRequests: disposition === "needs_review" ? [{ kind: "review", ownerClass: "human", summary: "Review the completed work." }] : [], artifacts: [] },
terminal: { schema: "paperclip.prp.terminal.v1", runTerminalState: "succeeded", turnTerminalState: "completed", reportedWorkDisposition: disposition }, turnId: "hire-turn" });
await finalizeNativeRun({ db, runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: false });
expect((await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, runId)))[0].phase).toBe("committed");
if (options.succeed !== false) await db.update(heartbeatRuns).set({ status: "succeeded", finishedAt: new Date("2020-01-01T00:02:00Z") }).where(eq(heartbeatRuns.id, runId));
const input = { companyId, issueId, runId };
return { ...input, input, agentId, userId, wakeId, prpEvent, reply };
}
const comments = (runId: string) => db.select().from(issueComments).where(eq(issueComments.createdByRunId, runId));
const issue = async (issueId: string) => (await db.select().from(issues).where(eq(issues.id, issueId)))[0];
async function completion(f: Awaited<ReturnType<typeof fixture>>) {
const taskId = randomUUID(), deliveryId = randomUUID();
await instanceSettingsService(db).updateExperimental({ enableAgentChat: true });
await db.insert(issues).values({ id: taskId, companyId: f.companyId, title: "Completed review", status: "done", statusVersion: 2 });
await db.insert(chatTaskHandoffs).values({ companyId: f.companyId, taskId, conversationId: f.issueId,
agentId: f.agentId, sessionGeneration: 0 });
await db.insert(chatCompletionDeliveries).values({ id: deliveryId, companyId: f.companyId, taskId,
statusVersion: 2, status: "queued", targetRunId: f.runId, nextAttemptAt: new Date(0) });
await db.update(heartbeatRuns).set({ finishedAt: new Date() }).where(and(eq(heartbeatRuns.id, f.runId), eq(heartbeatRuns.status, "succeeded")));
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId: f.issueId, source: "automation", wakeReason: "chat_task_completed",
conversationMode: true, conversationSessionGeneration: 0, chatCompletionDeliveryIds: [deliveryId], chatCompletionUpdates: [{ taskId }] } })
.where(eq(heartbeatRuns.id, f.runId));
const wakeup = vi.fn(async (_agentId: string, options: { idempotencyKey: string }) => {
await db.insert(agentWakeupRequests).values({ companyId: f.companyId, agentId: f.agentId, source: "automation",
status: "skipped", reason: "heartbeat.daily_run_limit", idempotencyKey: options.idempotencyKey });
return null;
});
await db.insert(agentWakeupRequests).values({ companyId: f.companyId, agentId: f.agentId, source: "automation",
status: "completed", reason: "chat_task_completed", idempotencyKey: `chat-completion:${deliveryId}:0`, runId: f.runId });
const delivery = async () => (await db.select().from(chatCompletionDeliveries).where(eq(chatCompletionDeliveries.id, deliveryId)))[0];
return { taskId, deliveryId, wakeup, delivery, service: chatCompletionDeliveryService(db, { wakeup }) };
}
it("preserves accepted native callback presentation when outbox retry runs first at the daily cap", async () => {
const f = await fixture(), outbox = await completion(f);
const work = nativeResponsePresentationWork(db);
const [entry] = await work.load(f.input);
expect(entry).toMatchObject({ id: f.runId });
await outbox.service.deliver(outbox.deliveryId);
await expect(work.check(entry)).resolves.toBeNull();
const [comment] = await comments(f.runId);
expect(comment).toMatchObject({ body: f.reply, authorAgentId: f.agentId, createdByRunId: f.runId });
expect(await comments(f.runId)).toHaveLength(1);
expect(await issue(f.issueId)).toMatchObject({ status: "in_review", conversationState: "waiting" });
expect(await outbox.delivery()).toMatchObject({ status: "delivered", targetRunId: f.runId, attempts: 0, responseCommentId: comment.id });
expect(outbox.wakeup).not.toHaveBeenCalled();
});
it.each([false, true])("wakes the real reply worker only after outer commit, becomes idle and persists once (rollback=%s)", async rollback => {
const f = await fixture(true, { succeed: false }), outbox = await completion(f);
let queries = 0;
const debug = db.$client.options.debug;
db.$client.options.debug = () => { queries++; };
const errors: unknown[] = [];
const coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => true, onError: error => errors.push(error) });
const worker = nativeResponsePresentationWork(db).register(coordinator, () => true);
const quiet = () => vi.waitFor(() => {
expect(coordinator.nextWakeAt()).toBeNull();
expect(idleWorkSnapshot().active).toBe(0);
}, { timeout: 5000 });
const projectAndDefer = async (abort: boolean) => db.transaction(async tx => {
await tx.update(heartbeatRuns).set({ status: "succeeded", finishedAt: new Date() }).where(eq(heartbeatRuns.id, f.runId));
await chatCompletionDeliveryService(tx as unknown as Db, { wakeup: outbox.wakeup }).deliver(outbox.deliveryId);
expect(await comments(f.runId)).toEqual([]);
if (abort) throw new Error("fixture-outbox-rollback");
});
try {
await worker.ready; await quiet();
if (rollback) {
await expect(projectAndDefer(true)).rejects.toThrow("fixture-outbox-rollback");
await quiet();
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)))[0].status).toBe("running");
expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: f.runId, attempts: 0, nextAttemptAt: new Date(0) });
expect(await comments(f.runId)).toEqual([]);
}
await projectAndDefer(false);
await vi.waitFor(async () => expect(await comments(f.runId)).toHaveLength(1), { timeout: 5000 });
await quiet();
const [comment] = await comments(f.runId);
expect(comment).toMatchObject({ body: f.reply, authorAgentId: f.agentId, createdByRunId: f.runId });
expect(await issue(f.issueId)).toMatchObject({ status: "in_review", conversationState: "waiting" });
expect(await outbox.delivery()).toMatchObject({ status: "delivered", targetRunId: f.runId, attempts: 0, responseCommentId: comment.id });
const settledQueries = queries;
vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout"], shouldAdvanceTime: true });
await vi.advanceTimersByTimeAsync(60_000);
expect(queries).toBe(settledQueries);
expect(coordinator.nextWakeAt()).toBeNull();
vi.useRealTimers();
await outbox.service.deliver(outbox.deliveryId);
expect(await comments(f.runId)).toHaveLength(1);
expect(outbox.wakeup).not.toHaveBeenCalled();
expect(errors).toEqual([]);
} finally {
vi.useRealTimers(); worker.stop(); await coordinator.stop(); db.$client.options.debug = debug;
}
});
it.each(["failed", "cancelled", "missing-final", "no-accepted", "old-generation", "foreign-company", "foreign-agent"])(
"keeps the existing retry path for %s targets", async fault => {
const f = await fixture(), outbox = await completion(f);
if (fault === "failed" || fault === "cancelled") await db.update(heartbeatRuns).set({ status: fault }).where(eq(heartbeatRuns.id, f.runId));
if (fault === "missing-final") await db.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.runId));
if (fault === "no-accepted") await db.update(nativeRunResults).set({ schemaStatus: "rejected" }).where(eq(nativeRunResults.runId, f.runId));
if (fault === "old-generation") {
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
await db.update(heartbeatRuns).set({ contextSnapshot: { ...run.contextSnapshot, conversationSessionGeneration: 1 } }).where(eq(heartbeatRuns.id, f.runId));
}
let foreign: Awaited<ReturnType<typeof fixture>> | undefined;
if (fault === "foreign-company") {
foreign = await fixture();
await db.update(chatCompletionDeliveries).set({ targetRunId: foreign.runId }).where(eq(chatCompletionDeliveries.id, outbox.deliveryId));
}
if (fault === "foreign-agent") {
const otherAgent = randomUUID();
await db.insert(agents).values({ id: otherAgent, companyId: f.companyId, name: "Other agent", adapterType: "paperclip_runner" });
await db.update(heartbeatRuns).set({ agentId: otherAgent }).where(eq(heartbeatRuns.id, f.runId));
}
await outbox.service.deliver(outbox.deliveryId);
expect(outbox.wakeup).toHaveBeenCalledTimes(1);
expect(await outbox.delivery()).toMatchObject({ status: "pending", targetRunId: null, attempts: 1, responseCommentId: null });
expect(await comments(f.runId)).toEqual([]);
if (foreign) expect(await comments(foreign.runId)).toEqual([]);
},
);
it.each(["reset-source", "reopened-task"])("keeps %s delivery supersession", async fault => {
const f = await fixture(), outbox = await completion(f);
if (fault === "reset-source") await db.update(issues).set({ conversationSessionGeneration: 1 }).where(eq(issues.id, f.issueId));
else {
await issueService(db).update(outbox.taskId, { status: "todo" });
await issueService(db).update(outbox.taskId, { status: "done" });
expect(await issue(outbox.taskId)).toMatchObject({ status: "done", statusVersion: 4 });
}
await outbox.service.deliver(outbox.deliveryId);
expect(await outbox.delivery()).toMatchObject({ status: "superseded", attempts: 0, responseCommentId: null });
expect(outbox.wakeup).not.toHaveBeenCalled();
expect(await comments(f.runId)).toEqual([]);
});
it("lets an in-flight presentation acknowledge before expiry can retarget the delivery", async () => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
const f = await fixture(), outbox = await completion(f);
const suffix = randomUUID().replaceAll("-", ""), lockId = Number.parseInt(suffix.slice(0, 6), 16) + 1;
const trigger = `completion_reply_hold_${suffix}`;
let release!: () => void, acquired!: () => void;
const hold = new Promise<void>(resolve => { release = resolve; });
const locked = new Promise<void>(resolve => { acquired = resolve; });
let blocker: Promise<unknown> | undefined, presentation: Promise<unknown> | undefined, expiry: Promise<unknown> | undefined;
try {
await db.execute(sql.raw(`create function ${trigger}() returns trigger language plpgsql as $$
begin
if NEW.created_by_run_id = '${f.runId}'::uuid then perform pg_advisory_xact_lock(${lockId}); end if;
return NEW;
end $$`));
await db.execute(sql.raw(`create trigger ${trigger} before insert on issue_comments for each row execute function ${trigger}()`));
blocker = db.transaction(async tx => { await tx.execute(sql`select pg_advisory_xact_lock(${lockId}::bigint)`); acquired(); await hold; });
await Promise.race([locked, blocker]);
const work = nativeResponsePresentationWork(db), [entry] = await work.load(f.input);
presentation = work.check(entry); void presentation.catch(() => undefined);
await vi.waitFor(async () => {
const [row] = await db.execute(sql`select exists (select 1 from pg_locks
where locktype = 'advisory' and classid = 0 and objid = ${lockId}::oid and not granted
and database = (select oid from pg_database where datname = current_database())) as waiting`);
expect(row.waiting).toBe(true);
}, { timeout: 5000 });
vi.setSystemTime(started + 300_000);
let expirySettled = false;
expiry = outbox.service.deliver(outbox.deliveryId).then(() => { expirySettled = true; });
void expiry.catch(() => undefined);
await vi.waitFor(async () => {
const [row] = await db.execute(sql`select exists (select 1 from pg_stat_activity
where datname = current_database() and pid <> pg_backend_pid() and wait_event_type = 'Lock'
and query like '%from "issues"%' and query like '%for update%') as waiting`);
expect(expirySettled || row.waiting).toBe(true);
}, { timeout: 5000 });
expect(expirySettled).toBe(false);
expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: f.runId, attempts: 0, responseCommentId: null });
release(); await blocker; await presentation; await expiry;
const [comment] = await comments(f.runId);
expect(comment).toMatchObject({ body: f.reply, authorAgentId: f.agentId, createdByRunId: f.runId });
expect(await comments(f.runId)).toHaveLength(1);
expect(await outbox.delivery()).toMatchObject({ status: "delivered", targetRunId: f.runId, attempts: 0, responseCommentId: comment.id });
expect(outbox.wakeup).not.toHaveBeenCalled();
expect(await issue(f.issueId)).toMatchObject({ status: "in_review", conversationState: "waiting" });
} finally {
release?.(); await Promise.allSettled([blocker, presentation, expiry].filter((value): value is Promise<unknown> => Boolean(value)));
await db.execute(sql.raw(`drop trigger if exists ${trigger} on issue_comments`));
await db.execute(sql.raw(`drop function if exists ${trigger}()`));
vi.useRealTimers();
}
});
it("lets expiry win before presentation and releases the child lock before wake admission", async () => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
try {
const f = await fixture(), outbox = await completion(f), lockErrors: unknown[] = [];
const work = nativeResponsePresentationWork(db), [entry] = await work.load(f.input);
vi.setSystemTime(started + 300_000);
const wakeup = vi.fn(async (agentId: string, options: { idempotencyKey: string }) => {
try {
await db.transaction(async tx => {
await tx.execute(sql`set local lock_timeout = '500ms'`);
await tx.select({ id: issues.id }).from(issues).where(eq(issues.id, f.issueId)).for("update");
await tx.select({ id: issues.id }).from(issues).where(eq(issues.id, outbox.taskId)).for("update");
});
} catch (error) { lockErrors.push(error); throw error; }
return outbox.wakeup(agentId, options);
});
await chatCompletionDeliveryService(db, { wakeup }).deliver(outbox.deliveryId);
expect(lockErrors).toEqual([]);
expect(wakeup).toHaveBeenCalledTimes(1);
expect(await outbox.delivery()).toMatchObject({ status: "pending", targetRunId: null, attempts: 1, responseCommentId: null });
await expect(work.check(entry)).rejects.toThrow("chat_completion_superseded");
expect(await comments(f.runId)).toEqual([]);
expect(await issue(outbox.taskId)).toMatchObject({ status: "done", statusVersion: 2 });
} finally { vi.useRealTimers(); }
});
it("rolls back an expiry transition without waking or losing the original target", async () => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
try {
const f = await fixture(), outbox = await completion(f);
vi.setSystemTime(started + 300_000);
const rollback = new Error("fixture-expiry-rollback");
const connection = new Proxy(db, { get(target, key) {
return key === "transaction" ? async (callback: Parameters<Db["transaction"]>[0]) => target.transaction(async tx => {
await callback(tx); throw rollback;
}) : Reflect.get(target, key);
} });
await chatCompletionDeliveryService(connection, { wakeup: outbox.wakeup }).deliver(outbox.deliveryId);
expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: f.runId, attempts: 0, responseCommentId: null,
error: rollback.message });
expect(outbox.wakeup).not.toHaveBeenCalled();
expect(await comments(f.runId)).toEqual([]);
} finally { vi.useRealTimers(); }
});
it.each(["new-claim", "delivered", "retargeted", "superseded"])("respects a %s winner while expiry waits for the child lock", async winner => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
const f = await fixture(), outbox = await completion(f);
const replacementId = randomUUID();
await db.insert(heartbeatRuns).values({ id: replacementId, companyId: f.companyId, agentId: f.agentId, status: "queued" });
let release!: () => void, acquired!: () => void;
const hold = new Promise<void>(resolve => { release = resolve; });
const locked = new Promise<void>(resolve => { acquired = resolve; });
let first: Promise<unknown> | undefined, second: Promise<unknown> | undefined;
const blocker = db.transaction(async tx => {
await tx.select({ id: issues.id }).from(issues).where(eq(issues.id, outbox.taskId)).for("update"); acquired(); await hold;
if (winner === "delivered") await tx.update(chatCompletionDeliveries).set({ status: "delivered" }).where(eq(chatCompletionDeliveries.id, outbox.deliveryId));
if (winner === "retargeted") await tx.update(chatCompletionDeliveries).set({ targetRunId: replacementId, attempts: 1 })
.where(eq(chatCompletionDeliveries.id, outbox.deliveryId));
if (winner === "superseded") {
await tx.update(issues).set({ status: "todo", statusVersion: 3 }).where(eq(issues.id, outbox.taskId));
await tx.update(chatCompletionDeliveries).set({ status: "superseded" }).where(eq(chatCompletionDeliveries.id, outbox.deliveryId));
}
});
const waiting = async (count: number) => vi.waitFor(async () => {
const [row] = await db.execute(sql`select count(*)::int as waiting from pg_stat_activity
where datname = current_database() and pid <> pg_backend_pid() and wait_event_type = 'Lock'
and query like '%from "issues"%' and query like '%for update%'`);
expect(row.waiting).toBeGreaterThanOrEqual(count);
}, { timeout: 5000 });
try {
await Promise.race([locked, blocker]);
vi.setSystemTime(started + 300_000);
first = outbox.service.deliver(outbox.deliveryId); void first.catch(() => undefined);
await waiting(1);
if (winner === "new-claim") {
vi.setSystemTime(started + 360_000);
second = outbox.service.deliver(outbox.deliveryId); void second.catch(() => undefined);
await waiting(2);
}
release(); await blocker; await first; await second;
if (winner === "new-claim") {
expect(outbox.wakeup).toHaveBeenCalledTimes(1);
expect(await outbox.delivery()).toMatchObject({ status: "pending", targetRunId: null, attempts: 1 });
} else {
expect(outbox.wakeup).not.toHaveBeenCalled();
if (winner === "delivered") expect((await outbox.delivery()).status).toBe("delivered");
if (winner === "retargeted") expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: replacementId, attempts: 1 });
if (winner === "superseded") expect((await outbox.delivery()).status).toBe("superseded");
}
expect(await comments(f.runId)).toEqual([]);
} finally {
release?.(); await Promise.allSettled([blocker, first, second].filter((value): value is Promise<unknown> => Boolean(value)));
vi.useRealTimers();
}
});
it("rechecks the fresh delivery when retry holds the child lock before presentation reads", async () => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
const f = await fixture(), outbox = await completion(f);
const suffix = randomUUID().replaceAll("-", ""), lockId = Number.parseInt(suffix.slice(0, 6), 16) + 1;
const trigger = `completion_retry_hold_${suffix}`;
let release!: () => void, acquired!: () => void;
const hold = new Promise<void>(resolve => { release = resolve; });
const locked = new Promise<void>(resolve => { acquired = resolve; });
let blocker: Promise<unknown> | undefined, presentation: Promise<unknown> | undefined, expiry: Promise<unknown> | undefined;
try {
await db.execute(sql.raw(`create function ${trigger}() returns trigger language plpgsql as $$
begin
if OLD.id = '${outbox.deliveryId}'::uuid and NEW.target_run_id is null then perform pg_advisory_xact_lock(${lockId}); end if;
return NEW;
end $$`));
await db.execute(sql.raw(`create trigger ${trigger} before update of target_run_id on chat_completion_deliveries for each row execute function ${trigger}()`));
blocker = db.transaction(async tx => { await tx.execute(sql`select pg_advisory_xact_lock(${lockId}::bigint)`); acquired(); await hold; });
await Promise.race([locked, blocker]);
const work = nativeResponsePresentationWork(db), [entry] = await work.load(f.input);
vi.setSystemTime(started + 300_000);
expiry = outbox.service.deliver(outbox.deliveryId); void expiry.catch(() => undefined);
await vi.waitFor(async () => {
const [row] = await db.execute(sql`select exists (select 1 from pg_locks
where locktype = 'advisory' and classid = 0 and objid = ${lockId}::oid and not granted
and database = (select oid from pg_database where datname = current_database())) as waiting`);
expect(row.waiting).toBe(true);
}, { timeout: 5000 });
presentation = work.check(entry); void presentation.catch(() => undefined);
await vi.waitFor(async () => {
const [row] = await db.execute(sql`select exists (select 1 from pg_stat_activity
where datname = current_database() and pid <> pg_backend_pid() and wait_event_type = 'Lock'
and query like '%from "issues"%' and query like '%for update%') as waiting`);
expect(row.waiting).toBe(true);
}, { timeout: 5000 });
release(); await blocker; await expiry;
const [outcome] = await Promise.allSettled([presentation]);
expect(outcome.status).toBe("rejected");
if (outcome.status === "rejected") expect(outcome.reason.message).toBe("chat_completion_superseded");
expect(await comments(f.runId)).toEqual([]);
expect(await outbox.delivery()).toMatchObject({ status: "pending", targetRunId: null, attempts: 1, responseCommentId: null });
expect(outbox.wakeup).toHaveBeenCalledTimes(1);
} finally {
release?.(); await Promise.allSettled([blocker, presentation, expiry].filter((value): value is Promise<unknown> => Boolean(value)));
await db.execute(sql.raw(`drop trigger if exists ${trigger} on chat_completion_deliveries`));
await db.execute(sql.raw(`drop function if exists ${trigger}()`));
vi.useRealTimers();
}
});
it.each(["within-grace", "at-expiry"])("preserves a Done reassignment's completion event %s and still retries at expiry", async when => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
try {
const f = await fixture(), outbox = await completion(f), reassignedAgent = randomUUID();
await db.insert(agents).values({ id: reassignedAgent, companyId: f.companyId, name: "New task owner", adapterType: "paperclip_runner" });
await db.update(nativeRunResults).set({ canonicalSha256: "invalid-digest" }).where(eq(nativeRunResults.runId, f.runId));
await db.update(nativeRunFinalizations).set({ nextAttemptAt: new Date(started + 120_000) }).where(eq(nativeRunFinalizations.runId, f.runId));
const updated = await issueService(db).update(outbox.taskId, { assigneeAgentId: reassignedAgent });
expect(updated).toMatchObject({ status: "done", statusVersion: 3, assigneeAgentId: reassignedAgent });
expect(await outbox.delivery()).toMatchObject({ status: "queued", statusVersion: 2, targetRunId: f.runId, attempts: 0 });
if (when === "within-grace") {
await outbox.service.deliver(outbox.deliveryId);
expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: f.runId, attempts: 0,
nextAttemptAt: new Date(started + 120_000) });
expect(outbox.wakeup).not.toHaveBeenCalled();
}
vi.setSystemTime(started + 300_000);
await outbox.service.deliver(outbox.deliveryId);
expect(await outbox.delivery()).toMatchObject({ status: "pending", targetRunId: null, attempts: 1, statusVersion: 2 });
expect(await comments(f.runId)).toEqual([]);
expect(outbox.wakeup).toHaveBeenCalledTimes(1);
expect(await issue(outbox.taskId)).toMatchObject({ status: "done", statusVersion: 3, assigneeAgentId: reassignedAgent });
} finally { vi.useRealTimers(); }
});
it("returns the fresh saved reply receipt after waiting for the child lock", async () => {
const f = await fixture(), outbox = await completion(f);
const [saved] = await db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId,
authorAgentId: f.agentId, createdByRunId: f.runId, body: f.reply }).returning();
let release!: () => void, acquired!: () => void;
const hold = new Promise<void>(resolve => { release = resolve; });
const locked = new Promise<void>(resolve => { acquired = resolve; });
const blocker = db.transaction(async tx => {
await tx.select({ id: issues.id }).from(issues).where(eq(issues.id, outbox.taskId)).for("update"); acquired(); await hold;
await tx.update(chatCompletionDeliveries).set({ status: "delivered", responseCommentId: saved.id })
.where(eq(chatCompletionDeliveries.id, outbox.deliveryId));
});
let reader: Promise<Awaited<ReturnType<typeof existingChatCompletionReply>>> | undefined;
try {
await Promise.race([locked, blocker]);
reader = db.transaction(tx => existingChatCompletionReply(tx, f.runId, f.issueId)); void reader.catch(() => undefined);
await vi.waitFor(async () => {
const [row] = await db.execute(sql`select exists (select 1 from pg_stat_activity
where datname = current_database() and pid <> pg_backend_pid() and wait_event_type = 'Lock'
and query like '%from "issues"%' and query like '%for update%') as waiting`);
expect(row.waiting).toBe(true);
}, { timeout: 5000 });
release(); await blocker;
expect(await reader).toMatchObject({ id: saved.id, body: f.reply, createdByRunId: f.runId });
expect(await comments(f.runId)).toHaveLength(1);
} finally { release?.(); await Promise.allSettled([blocker, reader]); }
});
it("does not let an old hold shorten a newer claim's lease", async () => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
const f = await fixture(), outbox = await completion(f);
const notify = nativeResponseNotifications.notifyNativeResponsePresentation;
let release!: () => void, notified!: () => void;
const hold = new Promise<void>(resolve => { release = resolve; });
const ready = new Promise<void>(resolve => { notified = resolve; });
const spy = vi.spyOn(nativeResponseNotifications, "notifyNativeResponsePresentation").mockImplementationOnce(async (tx, scope) => {
await notify(tx, scope); notified(); await hold;
});
let old: Promise<unknown> | undefined;
try {
old = outbox.service.deliver(outbox.deliveryId); void old.catch(() => undefined);
await vi.waitFor(() => expect(spy).toHaveBeenCalledTimes(1), { timeout: 5000 }); await ready;
vi.setSystemTime(started + 60_000);
await outbox.service.deliver(outbox.deliveryId);
expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: f.runId, attempts: 0,
nextAttemptAt: new Date(started + 120_000) });
release(); await old;
expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: f.runId, attempts: 0,
nextAttemptAt: new Date(started + 120_000) });
expect(outbox.wakeup).not.toHaveBeenCalled();
expect(await comments(f.runId)).toEqual([]);
} finally { release?.(); await Promise.allSettled(old ? [old] : []); spy.mockRestore(); vi.useRealTimers(); }
});
it("saves a legitimate delayed reply before the fixed limit without another model run", async () => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
try {
const f = await fixture(), outbox = await completion(f);
for (const elapsed of [0, 60_000, 120_000, 240_000]) {
vi.setSystemTime(started + elapsed);
await outbox.service.deliver(outbox.deliveryId);
expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: f.runId, attempts: 0, responseCommentId: null });
expect(outbox.wakeup).not.toHaveBeenCalled();
}
vi.setSystemTime(started + 299_999);
const work = nativeResponsePresentationWork(db), [entry] = await work.load(f.input);
await expect(work.check(entry)).resolves.toBeNull();
const [comment] = await comments(f.runId);
expect(comment).toMatchObject({ body: f.reply, authorAgentId: f.agentId, createdByRunId: f.runId });
expect(await issue(f.issueId)).toMatchObject({ status: "in_review", conversationState: "waiting" });
expect(await outbox.delivery()).toMatchObject({ status: "delivered", targetRunId: f.runId, attempts: 0, responseCommentId: comment.id });
vi.setSystemTime(started + 300_000);
await outbox.service.deliver(outbox.deliveryId);
expect(await comments(f.runId)).toHaveLength(1);
expect(outbox.wakeup).not.toHaveBeenCalled();
} finally { vi.useRealTimers(); }
});
it("clamps an external native retry deadline to the immutable five-lease presentation limit", async () => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
try {
const f = await fixture(), outbox = await completion(f);
await db.update(nativeRunFinalizations).set({ nextAttemptAt: new Date(started + 86_400_000) })
.where(eq(nativeRunFinalizations.runId, f.runId));
await outbox.service.deliver(outbox.deliveryId);
expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: f.runId, attempts: 0,
nextAttemptAt: new Date(started + 300_000) });
expect(outbox.wakeup).not.toHaveBeenCalled();
expect(await comments(f.runId)).toEqual([]);
vi.setSystemTime(started + 300_000);
await outbox.service.deliver(outbox.deliveryId);
expect(await outbox.delivery()).toMatchObject({ status: "pending", targetRunId: null, attempts: 1 });
expect(outbox.wakeup).toHaveBeenCalledTimes(1);
} finally { vi.useRealTimers(); }
});
it.each(["digest", "external-context"])("bounds permanent %s declines without sliding, publishing or changing accepted work", async fault => {
const started = Date.now();
vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(started);
try {
const f = await fixture(), outbox = await completion(f);
if (fault === "digest") await db.update(nativeRunResults).set({ canonicalSha256: "invalid-digest" }).where(eq(nativeRunResults.runId, f.runId));
else {
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
await db.update(heartbeatRuns).set({ contextSnapshot: { ...run.contextSnapshot, externalChatContinuation: true } }).where(eq(heartbeatRuns.id, f.runId));
}
const [acceptedBefore] = await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.runId));
const work = nativeResponsePresentationWork(db), [entry] = await work.load(f.input);
for (const elapsed of [0, 60_000, 120_000, 240_000]) {
vi.setSystemTime(started + elapsed);
await outbox.service.deliver(outbox.deliveryId);
await expect(work.check(entry)).resolves.toBeNull();
expect(await comments(f.runId)).toEqual([]);
expect(await outbox.delivery()).toMatchObject({ status: "queued", targetRunId: f.runId, attempts: 0,
responseCommentId: null, nextAttemptAt: new Date(started + Math.min(elapsed + 60_000, 300_000)) });
expect(outbox.wakeup).not.toHaveBeenCalled();
}
vi.setSystemTime(started + 299_999);
await outbox.service.deliver(outbox.deliveryId);
expect((await outbox.delivery()).nextAttemptAt).toEqual(new Date(started + 300_000));
vi.setSystemTime(started + 300_000);
await outbox.service.deliver(outbox.deliveryId);
expect(await outbox.delivery()).toMatchObject({ status: "pending", targetRunId: null, attempts: 1, responseCommentId: null });
expect(outbox.wakeup).toHaveBeenCalledTimes(1);
expect(await comments(f.runId)).toEqual([]);
expect((await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.runId)))[0]).toEqual(acceptedBefore);
expect(await issue(outbox.taskId)).toMatchObject({ status: "done", statusVersion: 2 });
} finally { vi.useRealTimers(); }
});
it("finds a committed final Agent Chat turn, persists the exact reply once and only then settles waiting", async () => {
const f = await fixture();
expect(await comments(f.runId)).toEqual([]);
expect(await issue(f.issueId)).toMatchObject({ status: "in_progress", conversationState: "active" });
const work = nativeResponsePresentationWork(db), entries = await work.load(f.input);
expect(entries).toEqual([expect.objectContaining({ id: f.runId })]);
await expect(work.check(entries[0])).resolves.toBeNull();
expect(await comments(f.runId)).toEqual([expect.objectContaining({ body: f.reply, authorAgentId: f.agentId, createdByRunId: f.runId })]);
expect(await issue(f.issueId)).toMatchObject({ status: "in_review", conversationState: "waiting" });
expect(await work.load(f.input)).toEqual([]);
await expect(recoverCommittedNativeChatResponse(db, f.input)).resolves.toBe(false);
expect(await comments(f.runId)).toHaveLength(1);
});
it("keeps a later user message actionable after recovering the earlier exact reply", async () => {
const f = await fixture();
await db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId, authorUserId: f.userId, body: "Another instruction",
createdAt: new Date("2020-01-01T00:01:00Z") });
await expect(recoverCommittedNativeChatResponse(db, f.input)).resolves.toBe(true);
expect(await comments(f.runId)).toHaveLength(1);
expect(await issue(f.issueId)).toMatchObject({ status: "in_progress", conversationState: "active" });
});
it("does not duplicate a reply under concurrent recovery", async () => {
const f = await fixture();
expect((await Promise.all([recoverCommittedNativeChatResponse(db, f.input), recoverCommittedNativeChatResponse(db, f.input)])).sort())
.toEqual([false, true]);
expect(await comments(f.runId)).toHaveLength(1);
});
it.each(["skip", "generation", "agent", "user", "execution", "waiting", "turn", "event-run", "session", "channel", "kind", "digest", "partial"])(
"keeps %s mismatches unchanged instead of publishing an unauthorized or incomplete reply", async fault => {
const f = await fixture();
if (["skip", "generation"].includes(fault)) await db.update(heartbeatRuns).set({ contextSnapshot: { issueId: f.issueId,
source: "issue.comment", wakeCommentId: f.wakeId, conversationSessionGeneration: fault === "generation" ? 1 : 0,
...(fault === "skip" ? { skipIssueComment: true } : {}) } }).where(eq(heartbeatRuns.id, f.runId));
if (fault === "user") await db.update(heartbeatRuns).set({ responsibleUserId: "another-user" }).where(eq(heartbeatRuns.id, f.runId));
if (fault === "agent") {
const other = randomUUID(); await db.insert(agents).values({ id: other, companyId: f.companyId, name: "Other agent" });
await db.update(issues).set({ conversationAgentId: other, assigneeAgentId: other }).where(eq(issues.id, f.issueId));
}
if (fault === "execution") {
const other = randomUUID(); await db.insert(heartbeatRuns).values({ id: other, companyId: f.companyId, agentId: f.agentId });
await db.update(issues).set({ executionRunId: other }).where(eq(issues.id, f.issueId));
}
if (fault === "waiting") await db.update(issues).set({ executionRunId: null, status: "in_review", conversationState: "waiting" }).where(eq(issues.id, f.issueId));
if (["turn", "event-run", "session", "channel", "kind"].includes(fault)) {
const event = structuredClone(f.prpEvent);
if (fault === "turn") event.turnId = "foreign-turn";
if (fault === "event-run") event.runId = randomUUID();
if (fault === "session") event.normalizedSessionId = randomUUID();
if (fault === "channel") event.payload.channel = "analysis";
if (fault === "kind") event.payload.kind = "reasoning";
await db.update(heartbeatRunEvents).set({ payload: { prpEvent: event } }).where(and(eq(heartbeatRunEvents.runId, f.runId), eq(heartbeatRunEvents.seq, 170)));
}
if (fault === "digest") await db.update(nativeRunResults).set({ canonicalSha256: "invalid-digest" }).where(eq(nativeRunResults.runId, f.runId));
if (fault === "partial") await db.update(nativeRunFinalizations).set({ phase: "copying_back" }).where(eq(nativeRunFinalizations.runId, f.runId));
const before = await issue(f.issueId);
await expect(recoverCommittedNativeChatResponse(db, f.input)).resolves.toBe(false);
expect(await comments(f.runId)).toEqual([]); expect(await issue(f.issueId)).toEqual(before);
},
);
it.each([false, true])("preserves a selected reply without replacing or resurrecting it (deleted=%s)", async deleted => {
const f = await fixture();
const [saved] = await db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId, authorAgentId: f.agentId,
createdByRunId: f.runId, body: "Already selected", ...(deleted ? { deletedAt: new Date("2020-01-01T00:03:00Z") } : {}) }).returning();
await db.update(heartbeatRuns).set({ resultJson: { presentationDecision: { commentId: saved.id, commentAction: "create" } } }).where(eq(heartbeatRuns.id, f.runId));
await expect(recoverCommittedNativeChatResponse(db, f.input)).resolves.toBe(false);
expect(await comments(f.runId)).toEqual([saved]);
});
it("never crosses company scope and keeps ordinary internal task response recovery intact", async () => {
const f = await fixture(false);
await expect(recoverCommittedNativeChatResponse(db, { ...f.input, companyId: randomUUID() })).resolves.toBe(false);
const work = nativeResponsePresentationWork(db), entries = await work.load(f.input);
expect(entries).toHaveLength(1); await expect(work.check(entries[0])).resolves.toBeNull();
expect(await comments(f.runId)).toEqual([expect.objectContaining({ body: f.reply })]);
expect((await issue(f.issueId)).conversationAgentId).toBeNull();
});
it("recovers a deferred review reply once without resolving the review or rerunning work", async () => {
const f = await fixture(false, { disposition: "needs_review" });
const before = await issue(f.issueId);
const reviews = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, f.issueId));
expect(before.status).toBe("in_review");
expect(reviews).toEqual([expect.objectContaining({ status: "pending" })]);
expect(await comments(f.runId)).toEqual([]);
const work = nativeResponsePresentationWork(db), entries = await work.load(f.input);
expect(entries).toHaveLength(1);
await expect(work.check(entries[0])).resolves.toBeNull();
expect(await comments(f.runId)).toEqual([expect.objectContaining({ body: f.reply, createdByRunId: f.runId })]);
expect(await issue(f.issueId)).toEqual({ ...before, updatedAt: expect.any(Date) });
expect(await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, f.issueId))).toEqual(reviews);
expect(await work.load(f.input)).toEqual([]);
await expect(recoverCommittedNativeChatResponse(db, f.input)).resolves.toBe(false);
expect(await comments(f.runId)).toHaveLength(1);
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.nativeIssueId, f.issueId))).toHaveLength(1);
});
it.each(["missing-final", "wrong-turn", "digest", "skip-comment"])(
"keeps deferred review response recovery closed for %s", async fault => {
const f = await fixture(false, { disposition: "needs_review" });
if (fault === "missing-final") await db.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.runId));
if (fault === "wrong-turn") await db.update(heartbeatRunEvents).set({ payload: { prpEvent: { ...f.prpEvent, turnId: "another-turn" } } })
.where(and(eq(heartbeatRunEvents.runId, f.runId), eq(heartbeatRunEvents.seq, 170)));
if (fault === "digest") await db.update(nativeRunResults).set({ canonicalSha256: "invalid-digest" }).where(eq(nativeRunResults.runId, f.runId));
if (fault === "skip-comment") await db.update(heartbeatRuns).set({ contextSnapshot: { issueId: f.issueId, skipIssueComment: true } }).where(eq(heartbeatRuns.id, f.runId));
const before = await issue(f.issueId);
await expect(recoverCommittedNativeChatResponse(db, f.input)).resolves.toBe(false);
expect(await comments(f.runId)).toEqual([]);
expect(await issue(f.issueId)).toEqual(before);
},
);
it("rolls back a partial reply write without changing the waiting state or consuming recovery", async () => {
const f = await fixture(), abort = new Error("fixture-commit-aborted");
const failing = new Proxy(db, { get(target, key) { return key === "transaction"
? (callback: Parameters<Db["transaction"]>[0]) => db.transaction(async tx => { await callback(tx); throw abort; })
: Reflect.get(target, key); } });
await expect(recoverCommittedNativeChatResponse(failing, f.input)).rejects.toBe(abort);
expect(await comments(f.runId)).toEqual([]);
expect(await issue(f.issueId)).toMatchObject({ status: "in_progress", conversationState: "active" });
await expect(recoverCommittedNativeChatResponse(db, f.input)).resolves.toBe(true);
expect(await comments(f.runId)).toHaveLength(1);
});
});