Files
PaperClipAI/server/src/__tests__/heartbeat-comment-wake-batching.test.ts
T
DottaandPaperclip 813e808af4 Remove filesystem history authorization from workspace preparation (#16963)
Remove physical-root source/reader authorization and sticky directory classifications from workspace preparation. Preserve current task/run authority, app-record privacy, credential authorization, containment, and lifecycle/publication integrity.

Validate replacement selections against persisted new inputs and refresh verified checkout ownership so bounded reuse survives earlier workspace relocation. Add real PostgreSQL/Git regressions, a reproducible history-scaling benchmark, and documentation.

Full CI passed all 48 jobs and Greptile scored 5/5 at 3d5da6c259. Five preparation-guard medians improved from 20,512.99 ms to 52.22 ms on the 8,500-workspace fixture; these are not end-to-end startup timings.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-20 09:19:59 -05:00

3580 lines
123 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { createServer } from "node:http";
import { and, asc, eq, sql } from "drizzle-orm";
import { WebSocketServer } from "ws";
import { FAST_RESPONSE_AGENT_GUIDANCE } from "@paperclipai/shared";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
agents,
authUsers,
agentWakeupRequests,
chatActions,
chatConversations,
chatDeliveries,
chatEndpointResources,
chatEndpoints,
chatExternalPrincipals,
chatIdentityLinks,
chatMessageLinks,
chatPublications,
companies,
companyMemberships,
createDb,
heartbeatRuns,
issueComments,
issueRecoveryActions,
issueQuestionResponseDeliveries,
issueThreadInteractions,
issues,
issueTreeHolds,
nativeRunFinalizations,
toolApplications,
toolConnections,
} from "@paperclipai/db";
import { runningProcesses } from "../adapters/index.js";
import { heartbeatService } from "../services/heartbeat.ts";
import { issueThreadInteractionService } from "../services/issue-thread-interactions.js";
import { isRetiredExternalChatQuestionSource, questionResponseDeliveryService } from "../services/question-response-delivery.js";
import { SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY } from "../services/recovery/index.ts";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.ts";
import { parseWakePayloadFromMessage } from "./helpers/wake-message.ts";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported
? describe
: describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres heartbeat comment wake batching tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
async function waitFor(
condition: () => boolean | Promise<boolean>,
timeoutMs = 10_000,
intervalMs = 50,
) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
if (await condition()) return;
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
throw new Error("Timed out waiting for condition");
}
async function closeDbClient(db: ReturnType<typeof createDb> | undefined) {
await db?.$client?.end?.({ timeout: 0 });
}
async function createControlledGatewayServer(beforeComplete?: (turn: number) => Promise<void>) {
const server = createServer();
const wss = new WebSocketServer({ server });
const agentPayloads: Array<Record<string, unknown>> = [];
let firstWaitRelease: (() => void) | null = null;
let firstWaitGate = new Promise<void>((resolve) => {
firstWaitRelease = resolve;
});
let waitCount = 0;
wss.on("connection", (socket) => {
socket.send(
JSON.stringify({
type: "event",
event: "connect.challenge",
payload: { nonce: "nonce-123" },
}),
);
socket.on("message", async (raw) => {
const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
const frame = JSON.parse(text) as {
type: string;
id: string;
method: string;
params?: Record<string, unknown>;
};
if (frame.type !== "req") return;
if (frame.method === "connect") {
socket.send(
JSON.stringify({
type: "res",
id: frame.id,
ok: true,
payload: {
type: "hello-ok",
protocol: 3,
server: { version: "test", connId: "conn-1" },
features: {
methods: ["connect", "agent", "agent.wait"],
events: ["agent"],
},
snapshot: { version: 1, ts: Date.now() },
policy: {
maxPayload: 1_000_000,
maxBufferedBytes: 1_000_000,
tickIntervalMs: 30_000,
},
},
}),
);
return;
}
if (frame.method === "agent") {
agentPayloads.push((frame.params ?? {}) as Record<string, unknown>);
const runId =
typeof frame.params?.idempotencyKey === "string"
? frame.params.idempotencyKey
: `run-${agentPayloads.length}`;
socket.send(
JSON.stringify({
type: "res",
id: frame.id,
ok: true,
payload: {
runId,
status: "accepted",
acceptedAt: Date.now(),
},
}),
);
return;
}
if (frame.method === "agent.wait") {
waitCount += 1;
if (waitCount === 1) {
await firstWaitGate;
}
await beforeComplete?.(waitCount);
socket.send(
JSON.stringify({
type: "res",
id: frame.id,
ok: true,
payload: {
runId: frame.params?.runId,
status: "ok",
startedAt: 1,
endedAt: 2,
},
}),
);
}
});
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => resolve());
});
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Failed to resolve test server address");
}
return {
url: `ws://127.0.0.1:${address.port}`,
getAgentPayloads: () => agentPayloads,
releaseFirstWait: () => {
firstWaitRelease?.();
firstWaitRelease = null;
firstWaitGate = Promise.resolve();
},
close: async () => {
await new Promise<void>((resolve) => wss.close(() => resolve()));
await new Promise<void>((resolve) => server.close(() => resolve()));
},
};
}
describeEmbeddedPostgres("heartbeat comment wake batching", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<
ReturnType<typeof startEmbeddedPostgresTestDatabase>
> | null = null;
function wakeCompanyRow(
rowId: (typeof companies.$inferInsert)["id"],
issuePrefix: (typeof companies.$inferInsert)["issuePrefix"],
): typeof companies.$inferInsert {
return {
id: rowId,
name: "Paperclip",
issuePrefix,
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
};
}
function wakeUserCommentRow(
companyId: (typeof issueComments.$inferInsert)["companyId"],
issueId: (typeof issueComments.$inferInsert)["issueId"],
authorUserId: (typeof issueComments.$inferInsert)["authorUserId"],
body: (typeof issueComments.$inferInsert)["body"],
): typeof issueComments.$inferInsert {
return {
companyId,
issueId,
authorUserId,
body,
};
}
function wakeGatewayAgentRow(
rowId: (typeof agents.$inferInsert)["id"],
companyId: (typeof agents.$inferInsert)["companyId"],
name: (typeof agents.$inferInsert)["name"],
gateway: { url: string },
): typeof agents.$inferInsert {
return {
id: rowId,
companyId,
name,
role: "engineer",
status: "idle",
adapterType: "openclaw_gateway",
adapterConfig: {
url: gateway.url,
headers: {
"x-openclaw-token": "gateway-token",
},
payloadTemplate: {
message: "wake now",
},
waitTimeoutMs: 2_000,
},
runtimeConfig: {},
permissions: {},
};
}
function wakeIssueRow(
rowId: (typeof issues.$inferInsert)["id"],
companyId: (typeof issues.$inferInsert)["companyId"],
title: (typeof issues.$inferInsert)["title"],
assigneeAgentId: (typeof issues.$inferInsert)["assigneeAgentId"],
issuePrefix: string,
): typeof issues.$inferInsert {
return {
id: rowId,
companyId,
title,
status: "todo",
priority: "medium",
responsibleUserId: "responsible-user",
assigneeAgentId,
issueNumber: 1,
identifier: `${issuePrefix}-1`,
};
}
function selectDeferredWakeForAgent(
companyId: (typeof agentWakeupRequests.$inferSelect)["companyId"],
agentId: (typeof agentWakeupRequests.$inferSelect)["agentId"],
) {
return db
.select()
.from(agentWakeupRequests)
.where(
and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.agentId, agentId),
eq(agentWakeupRequests.status, "deferred_issue_execution"),
),
)
.then((rows) => rows[0] ?? null);
}
beforeAll(async () => {
const started = await startEmbeddedPostgresTestDatabase(
"paperclip-heartbeat-comment-wake-",
);
db = createDb(started.connectionString);
tempDb = started;
}, 120_000);
afterAll(async () => {
await heartbeatService(db).drainActiveRunExecutions();
await closeDbClient(db);
await tempDb?.cleanup();
});
afterEach(() => {
runningProcesses.clear();
});
async function grantFixtureWorkspaceAccess(companyId: string, userIds: string[]) {
await db.insert(authUsers).values(userIds.map(id => ({
id, name: "Fixture commenter", email: `${id}@example.test`, createdAt: new Date(), updatedAt: new Date(),
}))).onConflictDoNothing();
await db.insert(companyMemberships).values(userIds.map(principalId => ({
companyId, principalId, principalType: "user", status: "active", membershipRole: "member",
})));
}
async function readGatewayWakePayload(
payload: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const message = String(payload.message ?? "");
if (message.includes("```json\n")) {
return parseWakePayloadFromMessage(message);
}
const runId = typeof payload.idempotencyKey === "string"
? payload.idempotencyKey
: null;
if (!runId) throw new Error("Gateway payload did not include its run id");
const run = await db
.select({ contextSnapshot: heartbeatRuns.contextSnapshot })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, runId))
.then((rows) => rows[0] ?? null);
const context = run?.contextSnapshot;
const wake = context && typeof context === "object" && !Array.isArray(context)
? (context as Record<string, unknown>).paperclipWake
: null;
if (!wake || typeof wake !== "object" || Array.isArray(wake)) {
throw new Error("Gateway payload omitted JSON without a structured wake context");
}
return wake as Record<string, unknown>;
}
it("defers approval-approved wakes for a running issue so the assignee resumes after the run", async () => {
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const runId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await db.insert(agents).values({
id: agentId,
companyId,
name: "CEO",
role: "ceo",
status: "running",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId,
invocationSource: "assignment",
triggerDetail: "system",
status: "running",
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
});
runningProcesses.set(runId, {
child: {} as never,
graceSec: 0,
processGroupId: null,
});
await db.insert(issues).values({
id: issueId,
companyId,
title: "Hire an agent",
status: "blocked",
priority: "medium",
responsibleUserId: "responsible-user",
assigneeAgentId: agentId,
executionRunId: runId,
executionAgentNameKey: "ceo",
executionLockedAt: new Date(),
issueNumber: 1,
identifier: `${issuePrefix}-1`,
});
const followupRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "approval_approved",
payload: {
issueId,
approvalId: "approval-1",
approvalStatus: "approved",
},
contextSnapshot: {
issueId,
taskId: issueId,
approvalId: "approval-1",
approvalStatus: "approved",
wakeReason: "approval_approved",
},
requestedByActorType: "user",
requestedByActorId: "local-board",
});
expect(followupRun).toBeNull();
const deferred = await selectDeferredWakeForAgent(companyId, agentId);
expect(deferred).not.toBeNull();
expect(deferred?.reason).toBe("issue_execution_deferred");
expect(deferred?.payload).toMatchObject({
issueId,
approvalId: "approval-1",
approvalStatus: "approved",
});
expect(
(deferred?.payload as Record<string, unknown>)._paperclipWakeContext,
).toMatchObject({
issueId,
taskId: issueId,
approvalId: "approval-1",
approvalStatus: "approved",
wakeReason: "approval_approved",
});
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId));
expect(runs).toHaveLength(1);
expect(runs[0]?.id).toBe(runId);
});
it("defers recovery hand-back wakes until the resolving run exits", async () => {
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const runId = randomUUID();
const recoveryActionId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await db.insert(agents).values({
id: agentId,
companyId,
name: "Recovery owner",
role: "engineer",
status: "running",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId,
invocationSource: "assignment",
triggerDetail: "system",
status: "running",
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "source_scoped_recovery_action",
},
});
runningProcesses.set(runId, {
child: {} as never,
graceSec: 0,
processGroupId: null,
});
await db.insert(issues).values({
id: issueId,
companyId,
title: "Resume handed-back work",
status: "todo",
priority: "medium",
responsibleUserId: "responsible-user",
assigneeAgentId: agentId,
executionRunId: runId,
executionAgentNameKey: "recovery owner",
executionLockedAt: new Date(),
issueNumber: 1,
identifier: `${issuePrefix}-1`,
});
const followupRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_recovery_action_restored",
payload: {
issueId,
recoveryActionId,
mutation: "recovery_action_resolution",
},
contextSnapshot: {
issueId,
taskId: issueId,
recoveryActionId,
wakeReason: "issue_recovery_action_restored",
source: "issue.recovery_action_resolution",
},
requestedByActorType: "agent",
requestedByActorId: agentId,
});
expect(followupRun).toBeNull();
const deferred = await selectDeferredWakeForAgent(companyId, agentId);
expect(deferred).toMatchObject({
reason: "issue_execution_deferred",
runId: null,
payload: expect.objectContaining({
issueId,
recoveryActionId,
mutation: "recovery_action_resolution",
}),
});
expect(
(deferred?.payload as Record<string, unknown>)._paperclipWakeContext,
).toMatchObject({
issueId,
taskId: issueId,
recoveryActionId,
wakeReason: "issue_recovery_action_restored",
source: "issue.recovery_action_resolution",
});
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId));
expect(runs).toHaveLength(1);
expect(runs[0]?.id).toBe(runId);
});
it("batches deferred comment wakes and forwards the ordered batch to the next run", async () => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "user-1"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Gateway Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Batch wake comments", agentId, issuePrefix));
const comment1 = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "First comment"))
.returning()
.then((rows) => rows[0]);
const firstRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: comment1.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: comment1.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(firstRun).not.toBeNull();
await waitFor(() => gateway.getAgentPayloads().length === 1);
await db.insert(issueComments).values({
companyId,
issueId,
authorAgentId: agentId,
createdByRunId: firstRun?.id ?? null,
body: "Heartbeat acknowledged",
});
const comment2 = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "Second comment"))
.returning()
.then((rows) => rows[0]);
const comment3 = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "Third comment"))
.returning()
.then((rows) => rows[0]);
const secondRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: comment2.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: comment2.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
const thirdRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: comment3.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: comment3.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(secondRun).toBeNull();
expect(thirdRun).toBeNull();
await waitFor(async () => {
const deferred = await selectDeferredWakeForAgent(companyId, agentId);
return Boolean(deferred);
});
const deferredWake = await selectDeferredWakeForAgent(companyId, agentId);
const deferredContext = (
deferredWake?.payload as Record<string, unknown> | null
)?._paperclipWakeContext as Record<string, unknown> | undefined;
expect(deferredContext?.wakeCommentIds).toEqual([
comment2.id,
comment3.id,
]);
gateway.releaseFirstWait();
await waitFor(() => gateway.getAgentPayloads().length === 2);
const secondPayload = gateway.getAgentPayloads()[1] ?? {};
const secondRunId =
typeof secondPayload.idempotencyKey === "string"
? secondPayload.idempotencyKey
: null;
if (!secondRunId) {
throw new Error(
"Expected forwarded gateway payload to include an idempotencyKey run id",
);
}
await waitFor(async () => {
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId));
const statusesByRunId = new Map(
runs.map((run) => [run.id, run.status]),
);
return (
statusesByRunId.get(firstRun!.id) === "succeeded" &&
statusesByRunId.get(secondRunId) === "succeeded"
);
}, 90_000);
const promotedRun = await db
.select({ contextSnapshot: heartbeatRuns.contextSnapshot })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, secondRunId))
.then((rows) => rows[0]);
const promotedTaskMarkdown = String(
(promotedRun.contextSnapshot as Record<string, unknown> | null)
?.paperclipTaskMarkdown ?? "",
);
expect(promotedTaskMarkdown).toContain(
"Pending wake comments (oldest to newest):",
);
expect(promotedTaskMarkdown.indexOf("Second comment")).toBeLessThan(
promotedTaskMarkdown.indexOf("Third comment"),
);
expect(promotedTaskMarkdown).not.toContain("First comment");
expect(secondPayload.paperclip).toBeUndefined();
const secondWake = await readGatewayWakePayload(secondPayload);
expect(secondWake).toMatchObject({
commentIds: [comment2.id, comment3.id],
latestCommentId: comment3.id,
});
expect(String(secondPayload.message ?? "")).toContain("Second comment");
expect(String(secondPayload.message ?? "")).toContain("Third comment");
// A fresh gateway request receives full context; the wake delta stays bounded above.
expect(String(secondPayload.message ?? "")).toContain("First comment");
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it("cancels an empty deferred comment wake instead of promoting deleted input", async () => {
const gateway = await createControlledGatewayServer(async turn => {
if (turn === 1) await db.update(issues).set({ status: "done" }).where(eq(issues.id, issueId));
});
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "user-1"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Gateway Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Discard deferred follow-up", agentId, issuePrefix));
const firstComment = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "First comment"))
.returning()
.then((rows) => rows[0]);
const firstRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: firstComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: firstComment.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(firstRun).not.toBeNull();
await waitFor(async () => {
const current = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, firstRun!.id))
.then((rows) => rows[0] ?? null);
return current?.status === "running";
});
const discardedComment = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "Delete this before the current turn finishes"))
.returning()
.then((rows) => rows[0]);
expect(
await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: discardedComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: discardedComment.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
}),
).toBeNull();
await waitFor(async () =>
db
.select({ id: agentWakeupRequests.id })
.from(agentWakeupRequests)
.where(
and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.agentId, agentId),
eq(agentWakeupRequests.status, "deferred_issue_execution"),
),
)
.then((rows) => Boolean(rows[0])),
);
const deferredWake = await db
.select()
.from(agentWakeupRequests)
.where(
and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.agentId, agentId),
eq(agentWakeupRequests.status, "deferred_issue_execution"),
),
)
.then((rows) => rows[0]);
if (!deferredWake) throw new Error("Expected a deferred comment wake");
await db
.delete(issueComments)
.where(eq(issueComments.id, discardedComment.id));
gateway.releaseFirstWait();
await waitFor(async () => {
const wake = await db
.select({ status: agentWakeupRequests.status })
.from(agentWakeupRequests)
.where(eq(agentWakeupRequests.id, deferredWake.id))
.then((rows) => rows[0] ?? null);
return wake?.status === "cancelled";
}, 90_000);
await heartbeat.drainActiveRunExecutions();
expect(gateway.getAgentPayloads()).toHaveLength(1);
const runs = await db
.select({ id: heartbeatRuns.id })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId));
expect(runs.map((run) => run.id)).toEqual([firstRun!.id]);
} finally {
gateway.releaseFirstWait();
await heartbeat.drainActiveRunExecutions();
await gateway.close();
}
}, 120_000);
it("retains deferred comments for reconciliation after cancelling an unknown provider outcome", async () => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "user-1"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Gateway Agent", gateway));
await db.insert(issues).values({
id: issueId,
companyId,
title: "Interrupt queued comment",
status: "todo",
priority: "medium",
responsibleUserId: "responsible-user",
assigneeAgentId: agentId,
issueNumber: 2,
identifier: `${issuePrefix}-2`,
});
const comment1 = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "Start work"))
.returning()
.then((rows) => rows[0]);
const firstRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: comment1.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: comment1.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(firstRun).not.toBeNull();
await waitFor(() => gateway.getAgentPayloads().length === 1);
const queuedComment = await db
.insert(issueComments)
.values({
companyId,
issueId,
authorType: "user",
authorUserId: "user-1",
body: "Queued follow-up",
presentation: {
kind: "system_notice",
tone: "warning",
detailsDefaultOpen: false,
},
metadata: {
version: 1,
sections: [
{
rows: [
{
type: "key_value",
label: "Cause",
value: "successful_run_missing_state",
},
],
},
],
},
})
.returning()
.then((rows) => rows[0]);
const followupRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: queuedComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: queuedComment.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(followupRun).toBeNull();
await heartbeat.cancelRun(firstRun!.id);
gateway.releaseFirstWait();
await heartbeat.reconcileStrandedAssignedIssues();
expect(gateway.getAgentPayloads()).toHaveLength(1);
const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, agentId));
expect(runs).toEqual([expect.objectContaining({ id: firstRun!.id, status: "cancelled" })]);
const [action] = await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.sourceIssueId, issueId));
expect(action).toMatchObject({ cause: "legacy_execution_requires_reconciliation", ownerType: "board", returnOwnerAgentId: agentId });
const [retained] = await db.select().from(issueComments).where(eq(issueComments.id, queuedComment.id));
expect(retained?.body).toBe("Queued follow-up");
const wakes = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.agentId, agentId));
expect(wakes.some(wake => wake.payload?.commentId === queuedComment.id && wake.status === "deferred_issue_execution")).toBe(true);
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it("promotes deferred comment wakes after the active run closes the issue", async () => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "user-1"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Gateway Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Reopen after deferred comment", agentId, issuePrefix));
const comment1 = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "First comment"))
.returning()
.then((rows) => rows[0]);
const firstRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: comment1.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: comment1.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(firstRun).not.toBeNull();
await waitFor(async () => {
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, firstRun!.id))
.then((rows) => rows[0] ?? null);
return run?.status === "running";
});
await waitFor(() => gateway.getAgentPayloads().length === 1);
const comment2 = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "Please handle this follow-up after you finish"))
.returning()
.then((rows) => rows[0]);
const deferredRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: comment2.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: comment2.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(deferredRun).toBeNull();
await waitFor(async () => {
const deferred = await selectDeferredWakeForAgent(companyId, agentId);
return Boolean(deferred);
});
// Running records admission. Wait for provider acceptance before
// simulating completion by that provider, or startup correctly rejects
// the already-closed task before this scenario reaches its follow-up.
await waitFor(() => gateway.getAgentPayloads().length >= 1);
await db
.update(issues)
.set({
status: "done",
completedAt: new Date(),
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
updatedAt: new Date(),
})
.where(eq(issues.id, issueId));
gateway.releaseFirstWait();
await waitFor(() => gateway.getAgentPayloads().length >= 2, 90_000);
await waitFor(async () => {
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId))
.orderBy(asc(heartbeatRuns.createdAt));
const [initialRun, promotedRun] = runs;
return (
initialRun?.id === firstRun?.id &&
initialRun.status === "succeeded" &&
promotedRun?.status === "succeeded"
);
}, 90_000);
const reopenedIssue = await db
.select({
status: issues.status,
completedAt: issues.completedAt,
})
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
expect(reopenedIssue).toMatchObject({
status: "in_progress",
completedAt: null,
});
const secondPayload = gateway.getAgentPayloads()[1] ?? {};
expect(secondPayload.paperclip).toBeUndefined();
const secondWake = await readGatewayWakePayload(secondPayload);
expect(secondWake).toMatchObject({
reason: "issue_commented",
commentIds: [comment2.id],
latestCommentId: comment2.id,
issue: {
id: issueId,
identifier: `${issuePrefix}-1`,
title: "Reopen after deferred comment",
status: "in_progress",
priority: "medium",
},
});
expect(String(secondPayload.message ?? "")).toContain(
"Please handle this follow-up after you finish",
);
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it.each([
{ caseName: "delivers explicit agent feedback after completion", targetAssignee: true, terminalStatus: "done", explicitResume: true },
{ caseName: "cancels an assignee continuation without resume intent on completed work", targetAssignee: true, terminalStatus: "done", explicitResume: false },
{ caseName: "cancels an assignee continuation on cancelled work", targetAssignee: true, terminalStatus: "cancelled", explicitResume: true },
] as const)("$caseName", async ({ targetAssignee, terminalStatus, explicitResume }) => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const assigneeAgentId = randomUUID();
const mentionedAgentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
const targetAgentId = targetAssignee ? assigneeAgentId : mentionedAgentId;
const shouldReopen = targetAssignee && terminalStatus === "done" && explicitResume;
const commentingAgentId = targetAssignee ? mentionedAgentId : assigneeAgentId;
const wakeReason = "issue_commented";
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user"]);
await db.insert(agents).values([
{
id: assigneeAgentId,
companyId,
name: "Primary Agent",
role: "engineer",
status: "idle",
adapterType: "openclaw_gateway",
adapterConfig: {
url: gateway.url,
headers: {
"x-openclaw-token": "gateway-token",
},
payloadTemplate: {
message: "wake now",
},
waitTimeoutMs: 2_000,
},
runtimeConfig: {},
permissions: {},
},
{
id: mentionedAgentId,
companyId,
name: "Mentioned Agent",
role: "engineer",
status: "idle",
adapterType: "openclaw_gateway",
adapterConfig: {
url: gateway.url,
headers: {
"x-openclaw-token": "gateway-token",
},
payloadTemplate: {
message: "wake now",
},
waitTimeoutMs: 2_000,
},
runtimeConfig: {},
permissions: {},
},
]);
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Agent feedback at completion boundary", assigneeAgentId, issuePrefix));
const firstRun = await heartbeat.wakeup(assigneeAgentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
requestedByActorType: "system",
requestedByActorId: null,
});
expect(firstRun).not.toBeNull();
await waitFor(async () => {
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, firstRun!.id))
.then((rows) => rows[0] ?? null);
return run?.status === "running";
});
await waitFor(() => gateway.getAgentPayloads().length === 1);
const comment = await db
.insert(issueComments)
.values({
companyId,
issueId,
authorAgentId: commentingAgentId,
createdByRunId: targetAssignee ? null : firstRun?.id ?? null,
body: "@Mentioned Agent please review after I finish",
})
.returning()
.then((rows) => rows[0]);
const deferredRun = await heartbeat.wakeup(targetAgentId, {
source: "automation",
triggerDetail: "system",
reason: wakeReason,
payload: { issueId, commentId: comment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: comment.id,
wakeCommentId: comment.id,
wakeReason,
...(explicitResume ? { resumeIntent: true, followUpRequested: true } : {}),
source: "comment.mention",
},
requestedByActorType: "agent",
requestedByActorId: commentingAgentId,
});
expect(deferredRun).toBeNull();
await waitFor(async () => {
const deferred = await selectDeferredWakeForAgent(companyId, targetAgentId);
return Boolean(deferred);
});
// Running records admission. Wait for provider acceptance before
// simulating completion by that provider, or startup correctly rejects
// the already-closed task before this scenario reaches its follow-up.
await waitFor(() => gateway.getAgentPayloads().length >= 1);
await db
.update(issues)
.set({
status: terminalStatus,
completedAt: new Date(),
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
updatedAt: new Date(),
})
.where(eq(issues.id, issueId));
gateway.releaseFirstWait();
if (targetAssignee && !shouldReopen) {
await waitFor(async () => {
const cancelled = await db.select().from(agentWakeupRequests).where(and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.agentId, targetAgentId),
eq(agentWakeupRequests.status, "cancelled"),
));
return cancelled.some((wake) => wake.error === "Deferred execution wake no longer applies to a terminal task");
});
const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, companyId));
expect(runs).toEqual([expect.objectContaining({ id: firstRun!.id, status: "succeeded" })]);
expect(gateway.getAgentPayloads()).toHaveLength(1);
const [closedIssue] = await db.select().from(issues).where(eq(issues.id, issueId));
expect(closedIssue).toMatchObject({ status: terminalStatus, executionRunId: null });
expect(closedIssue.completedAt).not.toBeNull();
const [retainedComment] = await db.select().from(issueComments).where(eq(issueComments.id, comment.id));
expect(retainedComment.body).toContain("please review after I finish");
return;
}
await waitFor(() => gateway.getAgentPayloads().length === 2, 90_000);
await waitFor(async () => {
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.companyId, companyId));
return (
runs.length === 2 && runs.every((run) => run.status === "succeeded")
);
}, 90_000);
const continuation = (await db.select().from(heartbeatRuns).where(and(
eq(heartbeatRuns.companyId, companyId), eq(heartbeatRuns.agentId, targetAgentId),
))).find((run) => run.id !== firstRun!.id);
expect(continuation).toMatchObject({
agentId: targetAgentId,
contextSnapshot: expect.objectContaining({ issueId }),
});
const issueAfterPromotion = await db
.select({
status: issues.status,
completedAt: issues.completedAt,
assigneeAgentId: issues.assigneeAgentId,
})
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
expect(issueAfterPromotion).toMatchObject({
status: shouldReopen ? "in_progress" : "done",
assigneeAgentId,
});
if (shouldReopen) expect(issueAfterPromotion?.completedAt).toBeNull();
else expect(issueAfterPromotion?.completedAt).not.toBeNull();
const secondPayload = gateway.getAgentPayloads()[1] ?? {};
expect(secondPayload.paperclip).toBeUndefined();
const secondWake = await readGatewayWakePayload(secondPayload);
expect(secondWake).toMatchObject({
reason: wakeReason,
commentIds: [comment.id],
latestCommentId: comment.id,
issue: {
id: issueId,
identifier: `${issuePrefix}-1`,
title: "Agent feedback at completion boundary",
status: shouldReopen ? "in_progress" : "done",
priority: "medium",
},
});
expect(String(secondPayload.message ?? "")).toContain(
"please review after I finish",
);
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it("promotes an interaction continuation with its full authoritative source comment after removing a coalesced self-comment", async () => {
const gateway = await createControlledGatewayServer(async turn => {
if (turn === 2) await db.update(issues).set({ status: "done" }).where(eq(issues.id, issueId));
});
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const interactionId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "local-cli-user", "external-user"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Local CLI Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Interaction continuation survives self-comment filtering", agentId, issuePrefix));
const firstRun = await heartbeat.wakeup(agentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
requestedByActorType: "system",
requestedByActorId: null,
});
expect(firstRun).not.toBeNull();
await waitFor(async () => {
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, firstRun!.id))
.then((rows) => rows[0] ?? null);
return run?.status === "running";
});
const selfComment = await db
.insert(issueComments)
.values({
companyId,
issueId,
authorUserId: "local-cli-user",
createdByRunId: firstRun!.id,
body: "Completion note from the source run",
})
.returning()
.then((rows) => rows[0]);
const fullSourceInstruction = [
"Continue the external request after confirmation.",
"Preserve every requirement from the original provider message, including this deliberately late clause.",
"TRAILING-INSTRUCTION: reply with the exact final release identifier.",
].join("\n");
const sourceComment = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "external-user", fullSourceInstruction))
.returning()
.then((rows) => rows[0]);
expect(
await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: selfComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: selfComment.id,
wakeCommentId: selfComment.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "local-cli-user",
}),
).toBeNull();
expect(
await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: {
issueId,
interactionId,
interactionKind: "request_confirmation",
interactionStatus: "accepted",
sourceCommentId: sourceComment.id,
wakeCommentId: sourceComment.id,
wakeCommentIds: [sourceComment.id],
mutation: "interaction",
},
contextSnapshot: {
issueId,
taskId: issueId,
interactionId,
interactionKind: "request_confirmation",
interactionStatus: "accepted",
sourceCommentId: sourceComment.id,
wakeCommentId: sourceComment.id,
wakeCommentIds: [sourceComment.id],
wakeReason: "issue_commented",
source: "issue.interaction.respond",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
}),
).toBeNull();
gateway.releaseFirstWait();
await waitFor(async () => {
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId))
.orderBy(asc(heartbeatRuns.createdAt));
return (
runs.length === 2 &&
runs[0]?.status === "succeeded" &&
runs[1]?.status === "succeeded"
);
}, 90_000);
const promotedRun = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId))
.orderBy(asc(heartbeatRuns.createdAt))
.then((runs) => runs[1] ?? null);
expect(promotedRun?.contextSnapshot).toMatchObject({
interactionId,
interactionKind: "request_confirmation",
interactionStatus: "accepted",
sourceCommentId: sourceComment.id,
wakeCommentId: sourceComment.id,
wakeCommentIds: [sourceComment.id],
});
expect(promotedRun?.contextSnapshot).not.toMatchObject({
commentId: selfComment.id,
});
expect(gateway.getAgentPayloads()).toHaveLength(2);
const continuationWake = await readGatewayWakePayload(
gateway.getAgentPayloads()[1] ?? {},
);
expect(continuationWake?.commentIds).toEqual([sourceComment.id]);
expect(continuationWake?.comments).toEqual([
expect.objectContaining({
id: sourceComment.id,
issueId,
body: fullSourceInstruction,
bodyTruncated: false,
}),
]);
expect(String(gateway.getAgentPayloads()[1]?.message ?? "")).toContain(
"TRAILING-INSTRUCTION: reply with the exact final release identifier.",
);
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it.each(["linked", "revoked", "restart", "unbound_marker", "ordinary_cancellation"] as const)(
"cancels a parked native chat source and authorizes its dedicated answer continuation (%s)",
async (identityStatus) => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const sourceRunId = randomUUID();
const userId = `linked-slack-user-${randomUUID()}`;
const endpointId = randomUUID();
const conversationId = randomUUID();
const principalId = randomUUID();
const deliveryId = randomUUID();
const applicationId = randomUUID();
const connectionId = randomUUID();
const resourceId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await db.insert(agents).values({
id: agentId,
companyId,
name: "Local CLI Agent",
role: "engineer",
status: "running",
adapterType: "openclaw_gateway",
adapterConfig: {
url: gateway.url,
headers: { "x-openclaw-token": "gateway-token" },
payloadTemplate: { message: "wake now" },
waitTimeoutMs: 2_000,
},
runtimeConfig: {},
permissions: {},
});
await db.insert(issues).values({
id: issueId,
companyId,
title: "Continue an answered Slack question",
status: "in_progress",
priority: "medium",
responsibleUserId: "responsible-user",
assigneeAgentId: agentId,
issueNumber: 1,
identifier: `${issuePrefix}-1`,
});
const sourceComment = await db
.insert(issueComments)
.values({
companyId,
issueId,
authorType: "user",
authorUserId: userId,
body: "Build the release and preserve this full source instruction.",
})
.returning()
.then((rows) => rows[0]!);
// Model an admitted, linked Slack source, not just an untrusted source
// string. The real dispatch attestor must reconstruct this durable chain.
await db.insert(toolApplications).values({
id: applicationId,
companyId,
applicationKey: `chat:slack:${endpointId}`,
name: "Slack question",
type: "chat",
status: "active",
});
await db.insert(toolConnections).values({
id: connectionId,
companyId,
applicationId,
name: "Slack question",
uid: `chat-slack-${endpointId}`,
connectionPurpose: "channel",
transport: "chat_sdk",
status: "active",
enabled: true,
});
await db.insert(chatEndpoints).values({
id: endpointId,
companyId,
connectionId,
provider: "slack",
publicId: randomUUID(),
assignedAgentId: agentId,
status: "active",
providerAccountId: "slack-workspace",
allowDirectMessages: true,
allowUnlinkedPeople: false,
});
await db.insert(chatEndpointResources).values({
id: resourceId,
companyId,
endpointId,
type: "direct_message",
providerResourceId: "slack-person",
label: "Slack direct message",
availability: "available",
enabled: true,
});
await db.insert(chatConversations).values({
id: conversationId,
companyId,
endpointId,
resourceId,
issueId,
externalConversationId: "slack-person",
externalThreadId: "slack:slack-person:1",
externalLabel: "Slack direct message",
sessionGeneration: 1,
isDirectMessage: true,
communicationGuidance: "## Communication in Slack\nFrozen connection preference.",
state: "active",
});
await db.insert(chatExternalPrincipals).values({
id: principalId,
companyId,
provider: "slack",
providerAccountId: "slack-workspace",
externalId: "slack-person",
kind: "user",
});
await db.insert(chatIdentityLinks).values({
companyId,
endpointId,
principalId,
paperclipUserId: userId,
status: "linked",
});
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", userId]);
await db.insert(chatDeliveries).values({
id: deliveryId,
companyId,
endpointId,
conversationId,
principalId,
providerEventId: "slack-source-message",
deduplicationKey: "slack-source-message",
eventKind: "message",
normalizedEvent: {},
state: "processed",
attempts: 1,
processedAt: new Date(),
});
await db.insert(chatMessageLinks).values({
companyId,
endpointId,
conversationId,
deliveryId,
commentId: sourceComment.id,
providerMessageId: "slack-source-message",
direction: "inbound",
});
await db.insert(heartbeatRuns).values({
id: sourceRunId,
companyId,
agentId,
invocationSource: "assignment",
triggerDetail: "system",
status: "running",
runtimeMode: "native",
nativeIssueId: issueId,
driverKind: "codex",
startedAt: new Date(),
contextSnapshot: {
issueId,
taskId: issueId,
source: "chat:slack",
paperclipTaskCommunicationGuidance: "FORGED caller preference",
commentId: sourceComment.id,
wakeCommentId: sourceComment.id,
wakeCommentIds: [sourceComment.id],
paperclipHarnessCheckedOut: true,
paperclipWake: {
externalChatProvider: "slack",
checkedOutByHarness: true,
issue: { id: issueId, workMode: "standard" },
commentIds: [sourceComment.id],
},
},
});
await db.insert(nativeRunFinalizations).values({
runId: sourceRunId,
companyId,
issueId,
phase: "observed",
});
await db
.update(issues)
.set({
checkoutRunId: sourceRunId,
executionRunId: sourceRunId,
executionAgentNameKey: "localcliagent",
executionLockedAt: new Date(),
})
.where(eq(issues.id, issueId));
const interactions = issueThreadInteractionService(db);
const pending = await interactions.create(
{ id: issueId, companyId },
{
kind: "ask_user_questions",
continuationPolicy: "wake_assignee",
sourceRunId,
sourceCommentId: sourceComment.id,
payload: {
version: 1,
questions: [
{
id: "release",
prompt: "Continue the release?",
selectionMode: "single",
required: true,
options: [{ id: "yes", label: "Yes" }],
},
],
},
},
{ agentId, runId: sourceRunId },
);
const answered = await interactions.answerQuestions(
{ id: issueId, companyId, status: "in_progress" },
pending.id,
{ answers: [{ questionId: "release", optionIds: ["yes"] }] },
{ userId },
);
const publicationId = randomUUID();
await db.insert(chatPublications).values({
id: publicationId,
companyId,
endpointId,
conversationId,
issueId,
state: "published",
idempotencyKey: `question:${answered.id}`,
providerMessageId: "slack-question-card",
publishedAt: new Date(),
payload: {
text: "Continue the release?",
interactionId: answered.id,
},
});
await db.insert(chatActions).values({
companyId,
endpointId,
conversationId,
principalId,
kind: "question_answer",
status: "processed",
providerActionId: `answer:${answered.id}`,
payload: {
version: 1,
interactionId: answered.id,
publicationId,
questionId: "release",
optionId: "yes",
},
result: { interactionId: answered.id, interactionStatus: "answered" },
});
if (identityStatus === "revoked") {
await db
.update(chatIdentityLinks)
.set({ status: "revoked" })
.where(
and(
eq(chatIdentityLinks.companyId, companyId),
eq(chatIdentityLinks.principalId, principalId),
),
);
}
const retiredScope = { companyId, issueId, agentId, runId: sourceRunId };
if (identityStatus === "unbound_marker" || identityStatus === "ordinary_cancellation") {
await heartbeat.cancelRun(sourceRunId, "Fixture cancellation", {
errorCode: identityStatus === "unbound_marker" ? "external_chat_continuation" : "cancelled",
resultJson: { interactionId: randomUUID(), externalChatContinuation: true },
suppressImmediateRecovery: true,
});
expect(await isRetiredExternalChatQuestionSource(db, retiredScope)).toBe(false);
const incidents = await db.select().from(issueRecoveryActions).where(and(
eq(issueRecoveryActions.companyId, companyId),
eq(issueRecoveryActions.sourceIssueId, issueId),
));
expect(incidents).toEqual([expect.objectContaining({
cause: "native_continuation_requires_reconciliation",
})]);
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId))).toHaveLength(0);
expect(gateway.getAgentPayloads()).toHaveLength(0);
return;
}
let nativeResolverCalls = 0;
const resolver = async () => {
nativeResolverCalls += 1;
return "queued" as const;
};
if (identityStatus === "restart") {
const interruptedDelivery = questionResponseDeliveryService(db, {
heartbeat: {
wakeup: heartbeat.wakeup,
cancelRun: async (...args) => {
await heartbeat.cancelRun(...args);
// A lost caller result after the durable cancellation is a
// restart boundary, not a new failure incident or permission
// to promote generic work before the dedicated wake exists.
throw new Error("fixture_cancel_receipt_lost");
},
},
resolveNativeQuestion: resolver,
});
expect(await interruptedDelivery.deliver(answered.id)).toBeNull();
expect(await isRetiredExternalChatQuestionSource(db, retiredScope)).toBe(true);
await heartbeatService(db).reconcileStrandedAssignedIssues();
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId))).toHaveLength(0);
expect(await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.companyId, companyId))).toHaveLength(0);
}
const answerDelivery = questionResponseDeliveryService(db, {
heartbeat,
resolveNativeQuestion: resolver,
});
const outcome = await answerDelivery.deliver(answered.id);
expect(nativeResolverCalls).toBe(0);
expect(outcome).toMatchObject({
status: "fallback_queued",
mode: "wake_fallback",
});
expect(await isRetiredExternalChatQuestionSource(db, retiredScope)).toBe(true);
expect(await isRetiredExternalChatQuestionSource(db, { ...retiredScope, agentId: randomUUID() })).toBe(false);
expect(await isRetiredExternalChatQuestionSource(db, { ...retiredScope, issueId: randomUUID() })).toBe(false);
expect(await db.select().from(issueRecoveryActions).where(and(
eq(issueRecoveryActions.companyId, companyId),
eq(issueRecoveryActions.fingerprint, `native-continuation:${sourceRunId}`),
))).toHaveLength(0);
const duplicateOutcomes = await Promise.all([answerDelivery.deliver(answered.id), answerDelivery.deliver(answered.id)]);
expect(duplicateOutcomes.every(value => value?.duplicate === true)).toBe(true);
expect(await db.select().from(agentWakeupRequests).where(and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.idempotencyKey, `question-response:${answered.id}`),
))).toHaveLength(1);
await expect(
db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, sourceRunId)),
).resolves.toEqual([{ status: "cancelled" }]);
let continuationRunId: string | null = null;
await waitFor(async () => {
continuationRunId = await db
.select({ runId: agentWakeupRequests.runId })
.from(agentWakeupRequests)
.where(
and(
eq(agentWakeupRequests.companyId, companyId),
eq(
agentWakeupRequests.idempotencyKey,
`question-response:${answered.id}`,
),
),
)
.then((rows) => rows[0]?.runId ?? null);
return continuationRunId !== null;
}, 30_000);
expect(continuationRunId).not.toBe(sourceRunId);
if (identityStatus === "revoked") {
await heartbeat.drainActiveRunExecutions();
const [deniedRun] = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, continuationRunId!));
expect(deniedRun).toMatchObject({
status: "failed",
error: "reviewed_chat_execution_binding_not_authorized",
contextSnapshot: expect.objectContaining({
interactionId: answered.id,
sourceRunId,
}),
});
expect(deniedRun?.contextSnapshot).not.toHaveProperty(
"paperclipExternalChatQuestionResponse",
);
expect(gateway.getAgentPayloads()).toHaveLength(0);
return;
}
await waitFor(() => gateway.getAgentPayloads().length === 1, 30_000);
const continuationRun = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, continuationRunId!))
.then((rows) => rows[0] ?? null);
expect(continuationRun).toMatchObject({
status: "running",
contextSnapshot: expect.objectContaining({
interactionId: answered.id,
sourceRunId,
sourceCommentId: sourceComment.id,
externalChatContinuation: true,
wakeCommentId: sourceComment.id,
wakeCommentIds: [sourceComment.id],
paperclipExternalChatExecutionBound: true,
paperclipTaskCommunicationGuidance: `## Communication in Slack\nFrozen connection preference.\n\n${FAST_RESPONSE_AGENT_GUIDANCE}`,
paperclipExternalChatQuestionResponse: expect.objectContaining({
schema: "paperclip.external_chat_question_response.v1",
interactionId: answered.id,
sourceRunId,
sourceCommentId: sourceComment.id,
endpointId,
conversationId,
}),
}),
});
expect(String(gateway.getAgentPayloads()[0]?.message ?? "")).toContain(
"preserve this full source instruction",
);
const restoredPrompt = String(gateway.getAgentPayloads()[0]?.message ?? "");
expect(restoredPrompt.match(/Frozen connection preference\./g)).toHaveLength(1);
expect(restoredPrompt).not.toContain("FORGED caller preference");
expect(String(gateway.getAgentPayloads()[0]?.message ?? "")).toContain(
"Preserve the original request's exact-output constraints literally.",
);
expect(String(gateway.getAgentPayloads()[0]?.message ?? "")).toContain(
"Do not narrate Paperclip workflow, checkout, status, or completion bookkeeping.",
);
const continuationWake = await readGatewayWakePayload(
gateway.getAgentPayloads()[0] ?? {},
);
expect(continuationWake).toMatchObject({
externalChatProvider: "slack",
externalChatExecutionBound: true,
externalChatQuestionResponse: expect.objectContaining({
interactionId: answered.id,
sourceRunId,
endpointId,
conversationId,
}),
});
// The gateway's structured JSON is intentionally omitted when the
// wake prompt owns comments. The authoritative interaction answer is
// still carried in the rendered prompt.
expect(String(gateway.getAgentPayloads()[0]?.message ?? "")).toContain(
"Continue the release?: Yes",
);
gateway.releaseFirstWait();
await waitFor(async () => {
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, continuationRunId!))
.then((rows) => rows[0] ?? null);
return run?.status === "succeeded";
}, 30_000);
await waitFor(async () =>
db
.select({ executionRunId: issues.executionRunId })
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0]?.executionRunId === null),
);
await expect(
db
.select({ status: issueQuestionResponseDeliveries.status })
.from(issueQuestionResponseDeliveries)
.where(
eq(issueQuestionResponseDeliveries.interactionId, answered.id),
),
).resolves.toEqual([{ status: "fallback_queued" }]);
await expect(
db
.select({ status: issueThreadInteractions.status })
.from(issueThreadInteractions)
.where(eq(issueThreadInteractions.id, answered.id)),
).resolves.toEqual([{ status: "answered" }]);
} finally {
gateway.releaseFirstWait();
await heartbeat.drainActiveRunExecutions();
await gateway.close();
}
},
120_000,
);
it("still reopens a finished issue when a deferred batch mixes self-authored and human comments", async () => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "local-cli-user", "user-1"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Local CLI Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Human follow-up must survive mixed deferred batches", agentId, issuePrefix));
const firstRun = await heartbeat.wakeup(agentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
requestedByActorType: "system",
requestedByActorId: null,
});
expect(firstRun).not.toBeNull();
await waitFor(async () => {
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, firstRun!.id))
.then((rows) => rows[0] ?? null);
return run?.status === "running";
});
const selfComment = await db
.insert(issueComments)
.values({
companyId,
issueId,
authorUserId: "local-cli-user",
createdByRunId: firstRun?.id ?? null,
body: "Closing note from the same run",
})
.returning()
.then((rows) => rows[0]);
const firstDeferredRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: selfComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: selfComment.id,
wakeCommentId: selfComment.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "local-cli-user",
});
expect(firstDeferredRun).toBeNull();
const humanComment = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "Real follow-up from a human after the run closes"))
.returning()
.then((rows) => rows[0]);
const secondDeferredRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: humanComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: humanComment.id,
wakeCommentId: humanComment.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(secondDeferredRun).toBeNull();
await waitFor(async () => {
const deferred = await selectDeferredWakeForAgent(companyId, agentId);
return Boolean(deferred);
});
// Running records admission. Wait for provider acceptance before
// simulating completion by that provider, or startup correctly rejects
// the already-closed task before this scenario reaches its follow-up.
await waitFor(() => gateway.getAgentPayloads().length >= 1);
await db
.update(issues)
.set({
status: "done",
completedAt: new Date(),
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
updatedAt: new Date(),
})
.where(eq(issues.id, issueId));
gateway.releaseFirstWait();
await waitFor(() => gateway.getAgentPayloads().length >= 2, 90_000);
await waitFor(async () => {
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId))
.orderBy(asc(heartbeatRuns.createdAt));
const [initialRun, promotedRun] = runs;
return (
initialRun?.id === firstRun?.id &&
initialRun.status === "succeeded" &&
promotedRun?.status === "succeeded"
);
}, 90_000);
const issueAfterPromotion = await db
.select({
status: issues.status,
completedAt: issues.completedAt,
})
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
expect(issueAfterPromotion).toMatchObject({
status: "in_progress",
completedAt: null,
});
const secondPayload = gateway.getAgentPayloads()[1] ?? {};
expect(secondPayload.paperclip).toBeUndefined();
const secondWake = await readGatewayWakePayload(secondPayload);
expect(secondWake).toMatchObject({
reason: "issue_commented",
commentIds: [humanComment.id],
latestCommentId: humanComment.id,
issue: {
id: issueId,
identifier: `${issuePrefix}-1`,
title: "Human follow-up must survive mixed deferred batches",
status: "in_progress",
priority: "medium",
},
});
expect(String(secondPayload.message ?? "")).toContain(
"Real follow-up from a human after the run closes",
);
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it("cancels a deferred comment wake when its only queued comment is deleted before promotion", async () => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "user-1"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Gateway Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Deleted follow-up must not reopen", agentId, issuePrefix));
const firstRun = await heartbeat.wakeup(agentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
requestedByActorType: "system",
requestedByActorId: null,
});
expect(firstRun).not.toBeNull();
await waitFor(async () => {
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, firstRun!.id))
.then((rows) => rows[0] ?? null);
return run?.status === "running";
});
await waitFor(() => gateway.getAgentPayloads().length === 1);
const queuedComment = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "Please look at this once you finish"))
.returning()
.then((rows) => rows[0]);
const deferredRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: queuedComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: queuedComment.id,
wakeCommentId: queuedComment.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(deferredRun).toBeNull();
await waitFor(async () => {
const deferred = await selectDeferredWakeForAgent(companyId, agentId);
return Boolean(deferred);
});
const deferredWake = await db
.select({ id: agentWakeupRequests.id })
.from(agentWakeupRequests)
.where(
and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.agentId, agentId),
eq(agentWakeupRequests.status, "deferred_issue_execution"),
),
)
.then((rows) => rows[0] ?? null);
const deferredWakeId = deferredWake!.id;
// The author retracts the comment before the first run finishes, so the
// real comment-liveness query must find zero live comments left in the
// queued batch.
await db.update(issueComments).set({ deletedAt: new Date() }).where(eq(issueComments.id, queuedComment.id));
// Running records admission. Wait for provider acceptance before
// simulating completion by that provider, or startup correctly rejects
// the already-closed task before this scenario reaches its follow-up.
await waitFor(() => gateway.getAgentPayloads().length >= 1);
await db
.update(issues)
.set({
status: "done",
completedAt: new Date(),
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
updatedAt: new Date(),
})
.where(eq(issues.id, issueId));
gateway.releaseFirstWait();
await waitFor(async () => {
const [wake] = await db
.select({ status: agentWakeupRequests.status })
.from(agentWakeupRequests)
.where(eq(agentWakeupRequests.id, deferredWakeId));
return wake?.status === "cancelled";
}, 90_000);
const [cancelledWake] = await db
.select({ status: agentWakeupRequests.status, error: agentWakeupRequests.error })
.from(agentWakeupRequests)
.where(eq(agentWakeupRequests.id, deferredWakeId));
expect(cancelledWake).toMatchObject({
status: "cancelled",
error: "Queued messages were discarded before promotion",
});
// No live comment remained, so the queue must not promote a second run
// and the issue must stay in the state the first run's completion left it in.
expect(gateway.getAgentPayloads()).toHaveLength(1);
const issueAfterCompletion = await db
.select({ status: issues.status, completedAt: issues.completedAt })
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
expect(issueAfterCompletion).toMatchObject({ status: "done" });
expect(issueAfterCompletion?.completedAt).not.toBeNull();
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it("cancels a deferred comment wake when its only queued comment was authored by the finishing run", async () => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "local-cli-user"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Local CLI Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Self-authored note must not reopen", agentId, issuePrefix));
const firstRun = await heartbeat.wakeup(agentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
requestedByActorType: "system",
requestedByActorId: null,
});
expect(firstRun).not.toBeNull();
await waitFor(async () => {
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, firstRun!.id))
.then((rows) => rows[0] ?? null);
return run?.status === "running";
});
const selfComment = await db
.insert(issueComments)
.values({
companyId,
issueId,
authorUserId: "local-cli-user",
createdByRunId: firstRun?.id ?? null,
body: "Closing note from the same run",
})
.returning()
.then((rows) => rows[0]);
const deferredRun = await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: selfComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: selfComment.id,
wakeCommentId: selfComment.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "local-cli-user",
});
expect(deferredRun).toBeNull();
await waitFor(async () => {
const deferred = await selectDeferredWakeForAgent(companyId, agentId);
return Boolean(deferred);
});
const queuedWake = await db
.select({ id: agentWakeupRequests.id })
.from(agentWakeupRequests)
.where(
and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.agentId, agentId),
eq(agentWakeupRequests.status, "deferred_issue_execution"),
),
)
.then((rows) => rows[0] ?? null);
// Running records admission. Wait for provider acceptance before
// simulating completion by that provider, or startup correctly rejects
// the already-closed task before this scenario reaches its follow-up.
await waitFor(() => gateway.getAgentPayloads().length >= 1);
await db
.update(issues)
.set({
status: "done",
completedAt: new Date(),
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
updatedAt: new Date(),
})
.where(eq(issues.id, issueId));
gateway.releaseFirstWait();
await waitFor(async () => {
const run = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, firstRun!.id))
.then((rows) => rows[0] ?? null);
const [wake] = await db
.select({ status: agentWakeupRequests.status })
.from(agentWakeupRequests)
.where(eq(agentWakeupRequests.id, queuedWake!.id));
return wake?.status === "cancelled" && run !== null && run.status !== "queued" && run.status !== "running";
}, 90_000);
const [cancelledWake] = await db
.select({ status: agentWakeupRequests.status, error: agentWakeupRequests.error })
.from(agentWakeupRequests)
.where(eq(agentWakeupRequests.id, queuedWake!.id));
expect(cancelledWake).toMatchObject({
status: "cancelled",
error: "Deferred wake contained only comments authored by the finishing run",
});
// The only queued comment came from the run that just finished, so the
// queue must not promote a second run for the same agent to re-read its
// own note, and the issue must stay in the state the run's completion left it in.
expect(gateway.getAgentPayloads()).toHaveLength(1);
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId));
expect(runs).toHaveLength(1);
expect(runs[0]).toMatchObject({ id: firstRun!.id, status: "succeeded" });
const issueAfterCompletion = await db
.select({ status: issues.status, completedAt: issues.completedAt })
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
expect(issueAfterCompletion).toMatchObject({ status: "done" });
expect(issueAfterCompletion?.completedAt).not.toBeNull();
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it("promotes an interaction continuation after removing a coalesced self-authored comment", async () => {
const gateway = await createControlledGatewayServer(async turn => {
if (turn === 2) await db.update(issues).set({ status: "done" }).where(eq(issues.id, issueId));
});
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const interactionId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "local-cli-user", "user-1"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Local CLI Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Interaction continuation survives self-comment filtering", agentId, issuePrefix));
const firstRun = await heartbeat.wakeup(agentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
requestedByActorType: "system",
requestedByActorId: null,
});
expect(firstRun).not.toBeNull();
await waitFor(async () => {
const run = await db
.select({ status: heartbeatRuns.status })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, firstRun!.id))
.then((rows) => rows[0] ?? null);
return run?.status === "running";
});
const selfComment = await db
.insert(issueComments)
.values({
companyId,
issueId,
authorUserId: "local-cli-user",
createdByRunId: firstRun!.id,
body: "Completion note from the source run",
})
.returning()
.then((rows) => rows[0]);
expect(await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId, commentId: selfComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: selfComment.id,
wakeCommentId: selfComment.id,
wakeReason: "issue_commented",
},
requestedByActorType: "user",
requestedByActorId: "local-cli-user",
})).toBeNull();
expect(await heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: {
issueId,
interactionId,
interactionKind: "request_confirmation",
interactionStatus: "accepted",
mutation: "interaction",
},
contextSnapshot: {
issueId,
taskId: issueId,
interactionId,
interactionKind: "request_confirmation",
interactionStatus: "accepted",
wakeReason: "issue_commented",
source: "issue.interaction.respond",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
})).toBeNull();
gateway.releaseFirstWait();
await waitFor(async () => {
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId))
.orderBy(asc(heartbeatRuns.createdAt));
return (
runs.length === 2 &&
runs[0]?.status === "succeeded" &&
runs[1]?.status === "succeeded"
);
}, 90_000);
const promotedRun = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId))
.orderBy(asc(heartbeatRuns.createdAt))
.then((runs) => runs[1] ?? null);
expect(promotedRun?.contextSnapshot).toMatchObject({
interactionId,
interactionKind: "request_confirmation",
interactionStatus: "accepted",
});
expect(promotedRun?.contextSnapshot).not.toMatchObject({
wakeCommentIds: expect.anything(),
});
expect(promotedRun?.contextSnapshot).not.toMatchObject({
commentId: selfComment.id,
});
expect(gateway.getAgentPayloads()).toHaveLength(2);
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it("queues exactly one follow-up run when an issue-bound run exits without a comment", async () => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Gateway Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Require a comment", agentId, issuePrefix));
const firstRun = await heartbeat.wakeup(agentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
requestedByActorType: "system",
requestedByActorId: null,
});
expect(firstRun).not.toBeNull();
await waitFor(() => gateway.getAgentPayloads().length === 1);
const firstPayload = gateway.getAgentPayloads()[0] ?? {};
expect(firstPayload.paperclip).toBeUndefined();
expect(String(firstPayload.message ?? "")).toContain(
"## Paperclip Wake Payload",
);
expect(String(firstPayload.message ?? "")).toContain(
"Do not switch to another issue until you have handled this wake.",
);
expect(String(firstPayload.message ?? "")).toContain(
"- checkout: already claimed by the harness for this run",
);
expect(String(firstPayload.message ?? "")).toContain(
"The harness already checked out this issue for the current run.",
);
expect(String(firstPayload.message ?? "")).toContain(
`${issuePrefix}-1 Require a comment`,
);
const firstWake = await readGatewayWakePayload(firstPayload);
expect(firstWake).toMatchObject({
reason: "issue_assigned",
checkedOutByHarness: true,
commentIds: [],
issue: {
id: issueId,
identifier: `${issuePrefix}-1`,
},
});
const checkedOutIssue = await db
.select({
status: issues.status,
checkoutRunId: issues.checkoutRunId,
executionRunId: issues.executionRunId,
})
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
expect(checkedOutIssue).toMatchObject({
status: "in_progress",
checkoutRunId: firstRun?.id,
executionRunId: firstRun?.id,
});
gateway.releaseFirstWait();
await waitFor(async () => {
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId))
.orderBy(asc(heartbeatRuns.createdAt));
return (
runs.length === 2 &&
runs.every((run) => run.status === "succeeded") &&
runs[0]?.issueCommentStatus === "retry_queued" &&
runs[1]?.issueCommentStatus === "retry_exhausted"
);
});
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId))
.orderBy(asc(heartbeatRuns.createdAt));
expect(runs).toHaveLength(2);
expect(runs[0]?.issueCommentStatus).toBe("retry_queued");
expect(runs[1]?.retryOfRunId).toBe(runs[0]?.id);
expect(runs[1]?.issueCommentStatus).toBe("retry_exhausted");
const comments = await db
.select()
.from(issueComments)
.where(eq(issueComments.issueId, issueId));
expect(comments).toHaveLength(0);
await waitFor(async () => {
const wakeups = await db
.select()
.from(agentWakeupRequests)
.where(
and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.agentId, agentId),
),
);
return wakeups.length >= 2;
});
const payloads = gateway.getAgentPayloads();
expect(payloads).toHaveLength(2);
expect(runs[1]?.contextSnapshot).toMatchObject({
retryReason: "missing_issue_comment",
});
expect(runs[1]?.contextSnapshot).not.toHaveProperty("modelProfile");
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 20_000);
it("ignores mentions during another agent run without deferring work", async () => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const primaryAgentId = randomUUID();
const mentionedAgentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user", "user-1"]);
await db.insert(agents).values([
{
id: primaryAgentId,
companyId,
name: "Primary Agent",
role: "engineer",
status: "idle",
adapterType: "openclaw_gateway",
adapterConfig: {
url: gateway.url,
headers: {
"x-openclaw-token": "gateway-token",
},
payloadTemplate: {
message: "wake now",
},
waitTimeoutMs: 2_000,
},
runtimeConfig: {},
permissions: {},
},
{
id: mentionedAgentId,
companyId,
name: "Mentioned Agent",
role: "engineer",
status: "idle",
adapterType: "openclaw_gateway",
adapterConfig: {
url: gateway.url,
headers: {
"x-openclaw-token": "gateway-token",
},
payloadTemplate: {
message: "wake now",
},
waitTimeoutMs: 2_000,
},
runtimeConfig: {},
permissions: {},
},
]);
await db.insert(issues).values({
id: issueId,
companyId,
title: "Prevent concurrent mention execution",
status: "todo",
priority: "high",
responsibleUserId: "responsible-user",
assigneeAgentId: primaryAgentId,
issueNumber: 1,
identifier: `${issuePrefix}-1`,
});
const primaryRun = await heartbeat.wakeup(primaryAgentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
requestedByActorType: "system",
requestedByActorId: null,
});
expect(primaryRun).not.toBeNull();
await waitFor(() => gateway.getAgentPayloads().length === 1);
const mentionComment = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "@Mentioned Agent please inspect this after the current run."))
.returning()
.then((rows) => rows[0]);
const mentionRun = await heartbeat.wakeup(mentionedAgentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_comment_mentioned",
payload: { issueId, commentId: mentionComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: mentionComment.id,
wakeCommentId: mentionComment.id,
wakeReason: "issue_comment_mentioned",
source: "comment.mention",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(mentionRun).toBeNull();
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.agentId, mentionedAgentId))).toEqual([]);
expect(gateway.getAgentPayloads()).toHaveLength(1);
gateway.releaseFirstWait();
await waitFor(async () => {
const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, primaryAgentId));
// Terminal run status is persisted before comment policy and lock
// cleanup. Observe release before checking the final queue state.
const [issue] = await db.select({ executionRunId: issues.executionRunId }).from(issues).where(eq(issues.id, issueId));
return runs.length === 2 && runs.every((run) => run.status === "succeeded") && issue.executionRunId === null;
}, 90_000);
const mentionedRuns = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, mentionedAgentId));
expect(mentionedRuns).toEqual([]);
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.agentId, mentionedAgentId))).toEqual([]);
const issueAfterMention = await db
.select({
assigneeAgentId: issues.assigneeAgentId,
executionRunId: issues.executionRunId,
executionAgentNameKey: issues.executionAgentNameKey,
})
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
expect(issueAfterMention?.assigneeAgentId).toBe(primaryAgentId);
expect(issueAfterMention?.executionRunId).toBeNull();
expect(issueAfterMention?.executionAgentNameKey).not.toBe(
"mentioned agent",
);
const primaryRuns = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, primaryAgentId))
.orderBy(asc(heartbeatRuns.createdAt));
expect(primaryRuns).toHaveLength(2);
expect(primaryRuns[0]?.issueCommentStatus).toBe("retry_queued");
expect(primaryRuns[1]?.retryOfRunId).toBe(primaryRuns[0]?.id);
expect(primaryRuns[1]?.issueCommentStatus).toBe("retry_exhausted");
const missingCommentRetries = await db
.select()
.from(agentWakeupRequests)
.where(
and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.agentId, primaryAgentId),
eq(agentWakeupRequests.reason, "missing_issue_comment"),
),
);
expect(missingCommentRetries).toHaveLength(1);
expect(missingCommentRetries[0]?.payload).not.toHaveProperty(
"modelProfile",
);
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it("ignores direct legacy mention wakes without creating a request or executing an adapter", async () => {
const gateway = await createControlledGatewayServer();
const companyId = randomUUID();
const primaryAgentId = randomUUID();
const mentionedAgentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await db.insert(agents).values([
{
id: primaryAgentId,
companyId,
name: "Primary Agent",
role: "engineer",
status: "idle",
adapterType: "openclaw_gateway",
adapterConfig: {
url: gateway.url,
headers: {
"x-openclaw-token": "gateway-token",
},
payloadTemplate: {
message: "wake now",
},
waitTimeoutMs: 2_000,
},
runtimeConfig: {},
permissions: {},
},
{
id: mentionedAgentId,
companyId,
name: "Mentioned Agent",
role: "engineer",
status: "idle",
adapterType: "openclaw_gateway",
adapterConfig: {
url: gateway.url,
headers: {
"x-openclaw-token": "gateway-token",
},
payloadTemplate: {
message: "wake now",
},
waitTimeoutMs: 2_000,
},
runtimeConfig: {},
permissions: {},
},
]);
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Mention should not steal execution ownership", primaryAgentId, issuePrefix));
const mentionComment = await db
.insert(issueComments)
.values(wakeUserCommentRow(companyId, issueId, "user-1", "@Mentioned Agent please inspect this."))
.returning()
.then((rows) => rows[0]);
const mentionRun = await heartbeat.wakeup(mentionedAgentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_comment_mentioned",
payload: { issueId, commentId: mentionComment.id },
contextSnapshot: {
issueId,
taskId: issueId,
commentId: mentionComment.id,
wakeCommentId: mentionComment.id,
wakeReason: "issue_comment_mentioned",
source: "comment.mention",
},
requestedByActorType: "user",
requestedByActorId: "user-1",
});
expect(mentionRun).toBeNull();
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, mentionedAgentId))).toEqual([]);
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.agentId, mentionedAgentId))).toEqual([]);
expect(gateway.getAgentPayloads()).toEqual([]);
const issueDuringMention = await db
.select({
assigneeAgentId: issues.assigneeAgentId,
executionRunId: issues.executionRunId,
executionAgentNameKey: issues.executionAgentNameKey,
})
.from(issues)
.where(eq(issues.id, issueId))
.then((rows) => rows[0] ?? null);
expect(issueDuringMention).toMatchObject({
assigneeAgentId: primaryAgentId,
executionRunId: null,
executionAgentNameKey: null,
});
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 120_000);
it("treats the automatic run summary as fallback-only when the run already posted a comment", async () => {
const gateway = await createControlledGatewayServer(async turn => {
if (turn === 2) await db.update(issues).set({ status: "done" }).where(eq(issues.id, issueId));
});
const companyId = randomUUID();
const agentId = randomUUID();
const issueId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
try {
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await grantFixtureWorkspaceAccess(companyId, ["responsible-user"]);
await db.insert(agents).values(wakeGatewayAgentRow(agentId, companyId, "Gateway Agent", gateway));
await db.insert(issues).values(wakeIssueRow(issueId, companyId, "Use existing comment", agentId, issuePrefix));
const firstRun = await heartbeat.wakeup(agentId, {
source: "assignment",
triggerDetail: "system",
reason: "issue_assigned",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_assigned",
},
requestedByActorType: "system",
requestedByActorId: null,
});
expect(firstRun).not.toBeNull();
await waitFor(() => gateway.getAgentPayloads().length === 1);
await db.insert(issueComments).values({
companyId,
issueId,
authorAgentId: agentId,
authorUserId: null,
createdByRunId: firstRun!.id,
body: "Manual completion comment from the run.",
});
gateway.releaseFirstWait();
await waitFor(async () => {
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId));
const sourceRun = runs.find((run) => run.id === firstRun?.id);
return (
sourceRun?.status === "succeeded" &&
sourceRun.issueCommentStatus === "satisfied"
);
});
const runs = await db
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.agentId, agentId));
const sourceRun = runs.find((run) => run.id === firstRun?.id);
expect(sourceRun?.issueCommentStatus).toBe("satisfied");
expect(sourceRun?.issueCommentSatisfiedByCommentId).not.toBeNull();
await waitFor(async () => (await db.select().from(agentWakeupRequests)
.where(eq(agentWakeupRequests.companyId, companyId)))
.some(wake => wake.reason === "issue_disposition_repair"));
await heartbeat.drainActiveRunExecutions();
const comments = await db
.select()
.from(issueComments)
.where(eq(issueComments.issueId, issueId))
.orderBy(asc(issueComments.createdAt));
expect(
comments.some(
(comment) =>
comment.body === "Manual completion comment from the run.",
),
).toBe(true);
expect(comments.some(comment => comment.body === SUCCESSFUL_RUN_HANDOFF_REQUIRED_NOTICE_BODY)).toBe(false);
expect(
comments.every((comment) => !comment.body.startsWith("## Run summary")),
).toBe(true);
const wakeups = await db
.select()
.from(agentWakeupRequests)
.where(
and(
eq(agentWakeupRequests.companyId, companyId),
eq(agentWakeupRequests.agentId, agentId),
),
);
expect(
wakeups.some((wakeup) => wakeup.reason === "missing_issue_comment"),
).toBe(false);
expect(
wakeups.some(
(wakeup) => wakeup.reason === "issue_disposition_repair",
),
).toBe(true);
} finally {
gateway.releaseFirstWait();
await gateway.close();
}
}, 20_000);
it("fails a deferred wake whose agent no longer exists, then still promotes the next queued wake", async () => {
const companyId = randomUUID();
const finishingAgentId = randomUUID();
const validAgentId = randomUUID();
const missingAgentId = randomUUID();
const issueId = randomUUID();
const runId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
// Pin scheduling suppression off with the runtimeEnv test seam. Do not rely
// on the ambient PAPERCLIP_IN_WORKTREE value: startNextQueuedRunForAgent
// no-ops under suppression and would leave the promoted wake at "queued".
const heartbeat = heartbeatService(db, { runtimeEnv: {} });
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await db.insert(agents).values([
{
id: finishingAgentId,
companyId,
name: "Finishing Agent",
role: "engineer",
status: "idle",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: validAgentId,
companyId,
name: "Assignee Agent",
role: "engineer",
status: "idle",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
]);
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId: finishingAgentId,
invocationSource: "assignment",
triggerDetail: "system",
status: "running",
runtimeMode: "legacy",
startedAt: new Date(),
contextSnapshot: { issueId },
responsibleUserId: "responsible-user",
});
await db.insert(issues).values({
id: issueId,
companyId,
title: "Continue past a missing deferred agent",
status: "in_progress",
priority: "medium",
responsibleUserId: "responsible-user",
assigneeAgentId: validAgentId,
issueNumber: 1,
identifier: `${issuePrefix}-1`,
executionRunId: runId,
});
const missingAgentWakeId = randomUUID();
// The agent_id foreign key always holds in the running system, so a wake row
// can never outlive its agent through the application. Bypass the check for
// this one insert to pin the release code's defensive branch for that state.
await db.transaction(async (tx) => {
await tx.execute(sql`set local session_replication_role = 'replica'`);
await tx.insert(agentWakeupRequests).values({
id: missingAgentWakeId,
companyId,
agentId: missingAgentId,
source: "automation",
reason: "issue_commented",
status: "deferred_issue_execution",
requestedByActorType: "system",
requestedByActorId: "test",
requestedAt: new Date("2026-08-22T15:00:00.000Z"),
payload: { issueId },
});
});
const validWakeId = randomUUID();
await db.insert(agentWakeupRequests).values({
id: validWakeId,
companyId,
agentId: validAgentId,
source: "automation",
reason: "issue_commented",
status: "deferred_issue_execution",
requestedByActorType: "system",
requestedByActorId: "test",
requestedAt: new Date("2026-08-22T15:01:00.000Z"),
payload: { issueId },
});
// A plain legacy cancel now stops promotion early for board reconciliation
// (see legacyExecutionNeedsReconciliation in legacy-execution-recovery.ts).
// Cancel as an in-flight workspace wait instead. That shape still reaches
// the deferred-wake promotion loop under test.
await heartbeat.cancelRun(runId, undefined, {
errorCode: "workspace_busy",
resultJson: {
executionRecovery: { kind: "workspace_wait", providerWorkStarted: false },
},
});
const [missingAgentWake, validWake, issueRow] = await Promise.all([
db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, missingAgentWakeId)).then((rows) => rows[0]),
db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, validWakeId)).then((rows) => rows[0]),
db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0]),
]);
expect(missingAgentWake).toMatchObject({
status: "failed",
error: "Deferred wake could not be promoted: agent is not invokable",
});
// The promotion writes "queued", then releaseIssueExecutionAndPromote
// immediately calls startNextQueuedRunForAgent for the idle promoted
// agent, which claims the run in the same call. Assert the settled
// state, not the intermediate one.
expect(validWake?.status).toBe("claimed");
expect(validWake?.runId).not.toBeNull();
expect(issueRow?.executionRunId).toBe(validWake?.runId);
});
it("fails a deferred wake with the same status and error text when its agent belongs to another company", async () => {
const companyId = randomUUID();
const otherCompanyId = randomUUID();
const finishingAgentId = randomUUID();
const crossCompanyAgentId = randomUUID();
const issueId = randomUUID();
const runId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const otherIssuePrefix = `T${otherCompanyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
await db.insert(companies).values([
{
id: companyId,
name: "Paperclip",
issuePrefix,
requireBoardApprovalForNewAgents: false,
defaultResponsibleUserId: "responsible-user",
},
{
id: otherCompanyId,
name: "Other Paperclip",
issuePrefix: otherIssuePrefix,
requireBoardApprovalForNewAgents: false,
},
]);
await db.insert(agents).values([
{
id: finishingAgentId,
companyId,
name: "Finishing Agent",
role: "engineer",
status: "idle",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: crossCompanyAgentId,
companyId: otherCompanyId,
name: "Other Company Agent",
role: "engineer",
status: "idle",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
]);
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId: finishingAgentId,
invocationSource: "assignment",
triggerDetail: "system",
status: "running",
runtimeMode: "legacy",
startedAt: new Date(),
contextSnapshot: { issueId },
responsibleUserId: "responsible-user",
});
await db.insert(issues).values({
id: issueId,
companyId,
title: "Cross-company deferred agent",
status: "in_progress",
priority: "medium",
responsibleUserId: "responsible-user",
assigneeAgentId: finishingAgentId,
issueNumber: 1,
identifier: `${issuePrefix}-1`,
executionRunId: runId,
});
const wakeId = randomUUID();
await db.insert(agentWakeupRequests).values({
id: wakeId,
companyId,
agentId: crossCompanyAgentId,
source: "automation",
reason: "issue_commented",
status: "deferred_issue_execution",
requestedByActorType: "system",
requestedByActorId: "test",
payload: { issueId },
});
// A plain legacy cancel now stops promotion early for board reconciliation
// (see legacyExecutionNeedsReconciliation in legacy-execution-recovery.ts).
// Cancel as an in-flight workspace wait instead. That shape still reaches
// the deferred-wake promotion loop under test.
await heartbeat.cancelRun(runId, undefined, {
errorCode: "workspace_busy",
resultJson: {
executionRecovery: { kind: "workspace_wait", providerWorkStarted: false },
},
});
const wake = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, wakeId)).then((rows) => rows[0]);
expect(wake).toMatchObject({
status: "failed",
error: "Deferred wake could not be promoted: agent is not invokable",
});
});
it("cancels a deferred wake under an active pause hold, but promotes a verified hold interaction with the hold context", async () => {
const companyId = randomUUID();
const finishingAgentId = randomUUID();
const holdAgentId = randomUUID();
const issueId = randomUUID();
const runId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
// Pin scheduling suppression off with the runtimeEnv test seam. Do not rely
// on the ambient PAPERCLIP_IN_WORKTREE value: startNextQueuedRunForAgent
// no-ops under suppression and would leave the promoted wake at "queued".
const heartbeat = heartbeatService(db, { runtimeEnv: {} });
await db.insert(companies).values(wakeCompanyRow(companyId, issuePrefix));
await db.insert(agents).values([
{
id: finishingAgentId,
companyId,
name: "Finishing Agent",
role: "engineer",
status: "idle",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: holdAgentId,
companyId,
name: "Hold Interaction Agent",
role: "engineer",
status: "idle",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
]);
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId: finishingAgentId,
invocationSource: "assignment",
triggerDetail: "system",
status: "running",
runtimeMode: "legacy",
startedAt: new Date(),
contextSnapshot: { issueId },
responsibleUserId: "responsible-user",
});
await db.insert(issues).values({
id: issueId,
companyId,
title: "A pause hold gates a plain wake but not a verified one",
status: "in_progress",
priority: "medium",
responsibleUserId: "responsible-user",
assigneeAgentId: holdAgentId,
issueNumber: 1,
identifier: `${issuePrefix}-1`,
executionRunId: runId,
});
const [hold] = await db.insert(issueTreeHolds).values({
companyId,
rootIssueId: issueId,
mode: "pause",
status: "active",
reason: "Investigating a regression",
}).returning();
const plainWakeId = randomUUID();
await db.insert(agentWakeupRequests).values({
id: plainWakeId,
companyId,
agentId: finishingAgentId,
source: "automation",
reason: "issue_commented",
status: "deferred_issue_execution",
requestedByActorType: "system",
requestedByActorId: "test",
requestedAt: new Date("2026-08-22T15:00:00.000Z"),
payload: { issueId },
});
const holdComment = await db.insert(issueComments).values(wakeUserCommentRow(companyId, issueId, "hold-user", "Please continue despite the hold")).returning().then((rows) => rows[0]!);
const verifiedWakeId = randomUUID();
await db.insert(agentWakeupRequests).values({
id: verifiedWakeId,
companyId,
agentId: holdAgentId,
source: "issue_comment",
reason: "issue_commented",
status: "deferred_issue_execution",
requestedByActorType: "user",
requestedByActorId: "hold-user",
requestedAt: new Date("2026-08-22T15:01:00.000Z"),
payload: {
issueId,
commentId: holdComment.id,
_paperclipWakeContext: {
wakeReason: "issue_commented",
source: "issue.comment",
wakeCommentIds: [holdComment.id],
},
},
});
// A plain legacy cancel now stops promotion early for board reconciliation
// (see legacyExecutionNeedsReconciliation in legacy-execution-recovery.ts).
// Cancel as an in-flight workspace wait instead. That shape still reaches
// the deferred-wake promotion loop under test.
await heartbeat.cancelRun(runId, undefined, {
errorCode: "workspace_busy",
resultJson: {
executionRecovery: { kind: "workspace_wait", providerWorkStarted: false },
},
});
const [plainWake, verifiedWake] = await Promise.all([
db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, plainWakeId)).then((rows) => rows[0]),
db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, verifiedWakeId)).then((rows) => rows[0]),
]);
expect(plainWake).toMatchObject({
status: "cancelled",
error: "Deferred wake suppressed by active subtree pause hold",
});
// Same settle-then-assert reasoning as the missing-agent test above:
// the idle promoted agent's run is claimed synchronously.
expect(verifiedWake?.status).toBe("claimed");
const promotedRun = await db
.select({ contextSnapshot: heartbeatRuns.contextSnapshot })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, verifiedWake!.runId!))
.then((rows) => rows[0]);
expect(promotedRun?.contextSnapshot).toMatchObject({
treeHoldInteraction: true,
activeTreeHold: {
holdId: hold!.id,
rootIssueId: issueId,
mode: "pause",
reason: "Investigating a regression",
interaction: true,
},
});
});
it("rolls back the wake row, the run row, and the issue lock together when the responsible user cannot resolve", async () => {
const companyId = randomUUID();
const finishingAgentId = randomUUID();
const deferredAgentId = randomUUID();
const issueId = randomUUID();
const runId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const heartbeat = heartbeatService(db);
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix,
requireBoardApprovalForNewAgents: false,
// No defaultResponsibleUserId: the company default must not resolve this wake.
});
await db.insert(agents).values([
{
id: finishingAgentId,
companyId,
name: "Finishing Agent",
role: "engineer",
status: "idle",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: deferredAgentId,
companyId,
name: "Deferred Agent",
role: "engineer",
status: "idle",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
]);
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId: finishingAgentId,
invocationSource: "assignment",
triggerDetail: "system",
status: "running",
runtimeMode: "legacy",
startedAt: new Date(),
contextSnapshot: { issueId },
// No responsibleUserId: the finishing run itself must not resolve this wake.
});
await db.insert(issues).values({
id: issueId,
companyId,
title: "No responsible user can be resolved",
status: "in_progress",
priority: "medium",
assigneeAgentId: deferredAgentId,
issueNumber: 1,
identifier: `${issuePrefix}-1`,
executionRunId: runId,
// No responsibleUserId: the issue itself must not resolve this wake.
});
const wakeId = randomUUID();
await db.insert(agentWakeupRequests).values({
id: wakeId,
companyId,
agentId: deferredAgentId,
source: "automation",
reason: "issue_commented",
status: "deferred_issue_execution",
requestedByActorType: "system",
requestedByActorId: "test",
payload: { issueId },
});
// A plain legacy cancel now stops promotion early for board reconciliation
// (see legacyExecutionNeedsReconciliation in legacy-execution-recovery.ts).
// Cancel as an in-flight workspace wait instead. That shape still reaches
// the deferred-wake promotion loop under test.
await expect(
heartbeat.cancelRun(runId, undefined, {
errorCode: "workspace_busy",
resultJson: {
executionRecovery: { kind: "workspace_wait", providerWorkStarted: false },
},
}),
).rejects.toMatchObject({
status: 422,
details: expect.objectContaining({ code: "responsible_user_unresolved" }),
});
const [wake, issueRow, runs] = await Promise.all([
db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, wakeId)).then((rows) => rows[0]),
db.select().from(issues).where(eq(issues.id, issueId)).then((rows) => rows[0]),
db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, companyId)),
]);
expect(wake?.status).toBe("deferred_issue_execution");
expect(issueRow?.executionRunId).toBe(runId);
expect(runs).toHaveLength(1);
});
});