Merge commit '19a97bbe57d1ebd3691c70dd822488efe7e5d3e1' into codex/sleep-stack-c1

* commit '19a97bbe57d1ebd3691c70dd822488efe7e5d3e1':
  fix(server): park persisted deferred admission refusals
This commit is contained in:
Dotta committed 2026-10-30 20:50:39 -05:00
commit c0fe0feffb
5 files changed
+204 -25

No files matched your search

+12 -3
View File
@@ -1,24 +1,33 @@
import { HttpError } from "../errors.js";
import { and, eq, sql } from "drizzle-orm";
import { agentWakeupRequests, type Db } from "@paperclipai/db";
import { notifyDeferredExecutionWork } from "./deferred-execution-notifications.js";
/** Admission has saved this refusal on the caller's durable deferred receipt.
* HTTP callers still receive the existing 409; receipt recovery waits for its
* owning gate producer instead of treating the refusal as infrastructure debt. */
export class PersistedExecutionWaitRefusal extends HttpError {
constructor(message: string, details?: unknown) { super(409, message, details); }
}
export async function writeDeferredExecutionWait(db: Db, scope: { companyId: string; requestId: string },
condition: Record<string, unknown>, options: { replace?: boolean } = {}) {
return db.transaction(async tx => {
const [current] = await tx.select({ payload: agentWakeupRequests.payload }).from(agentWakeupRequests)
.where(and(eq(agentWakeupRequests.companyId, scope.companyId), eq(agentWakeupRequests.id, scope.requestId),
eq(agentWakeupRequests.status, "deferred_issue_execution"))).for("update");
if (!current) return;
if (!current) return false;
const previous = current.payload?.executionWait;
const previousReason = previous && typeof previous === "object" && !Array.isArray(previous)
? (previous as Record<string, unknown>).reason : undefined;
if (!previous || previousReason !== condition.reason) await notifyDeferredExecutionWork(tx, scope);
await tx.update(agentWakeupRequests).set({
const [saved] = await tx.update(agentWakeupRequests).set({
payload: sql`jsonb_set(coalesce(${agentWakeupRequests.payload}, '{}'::jsonb), '{executionWait}',
${options.replace ? sql`'{}'::jsonb` : sql`coalesce(${agentWakeupRequests.payload}->'executionWait', '{}'::jsonb)`}
|| ${JSON.stringify(condition)}::jsonb)`,
updatedAt: new Date(),
}).where(and(eq(agentWakeupRequests.companyId, scope.companyId), eq(agentWakeupRequests.id, scope.requestId),
eq(agentWakeupRequests.status, "deferred_issue_execution")));
eq(agentWakeupRequests.status, "deferred_issue_execution"))).returning({ id: agentWakeupRequests.id });
return Boolean(saved);
});
}
@@ -1,3 +1,4 @@
import { PersistedExecutionWaitRefusal } from "../deferred-execution-wait.js";
import { and, asc, eq, gt, inArray, isNull, or, sql } from "drizzle-orm";
import { agentWakeupRequests, type Db } from "@paperclipai/db";
import type { createDeliveryWorkCoordinator } from "../delivery-work-coordinator.js";
@@ -105,7 +106,11 @@ export function registerDeferredReceiptWork(
onError: error => { errors.push(error); },
});
else if (kind === "task_chat") await owner.settleTaskChat(scope);
else await owner.resume({ ...scope, onError: error => { errors.push(error); } });
else {
const failed = (error: unknown) => { if (!(error instanceof PersistedExecutionWaitRefusal)) errors.push(error); };
try { await owner.resume({ ...scope, onError: failed }); }
catch (error) { failed(error); }
}
if (errors.length) throw new AggregateError(errors, `Deferred receipt ${entry.id} could not settle`);
return next === null ? null : { ...entry, dueAt: next };
},
+15 -16
View File
@@ -1,4 +1,4 @@
import { writeDeferredExecutionWait } from "../deferred-execution-wait.js";
import { PersistedExecutionWaitRefusal, writeDeferredExecutionWait } from "../deferred-execution-wait.js";
import { notifyDeferredExecutionWork } from "../deferred-execution-notifications.js";
import type { DeferredReceiptRecoveryOptions } from "../deferred-execution-notifications.js";
import { notifyRunDispatch } from "../run-dispatch-notifications.js";
@@ -2519,17 +2519,17 @@ export function createHeartbeatQueue(db: Db, dependencies: HeartbeatQueueDepende
skipReason: string,
patch: Partial<typeof agentWakeupRequests.$inferInsert> = {},
waitCondition?: Record<string, unknown>,
) => {
): Promise<{ created: boolean; waitSaved?: boolean }> => {
// A temporary refusal must leave the handoff retryable, without burning
// its one durable receipt ID on a skipped wake.
if (caseRequest) return { created: false };
if (executionWaitRequestId) {
await writeDeferredExecutionWait(db, { companyId: agent.companyId, requestId: executionWaitRequestId }, {
const waitSaved = await writeDeferredExecutionWait(db, { companyId: agent.companyId, requestId: executionWaitRequestId }, {
reason: skipReason, message: patch.error ?? (skipReason === "issue_tree_hold_active"
? "This task is paused. Resume it to send your saved message."
: "Waiting for task execution to be enabled. Your message is saved."),
});
return { created: false };
return { created: false, waitSaved };
}
const request = {
...durableReceiptFields,
@@ -2801,31 +2801,30 @@ export function createHeartbeatQueue(db: Db, dependencies: HeartbeatQueueDepende
},
);
if (budgetBlock) {
await writeSkippedRequest("budget.blocked", { error: budgetBlock.reason }, {
const wait = await writeSkippedRequest("budget.blocked", { error: budgetBlock.reason }, {
scopeType: budgetBlock.scopeType, scopeId: budgetBlock.scopeId,
});
if (caseRequest) return null;
throw conflict(budgetBlock.reason, {
scopeType: budgetBlock.scopeType,
scopeId: budgetBlock.scopeId,
});
const details = { scopeType: budgetBlock.scopeType, scopeId: budgetBlock.scopeId };
throw wait.waitSaved ? new PersistedExecutionWaitRefusal(budgetBlock.reason, details)
: conflict(budgetBlock.reason, details);
}
// Setup delays execution, but must not discard work accepted by a caller.
const invokability = await getAgentInvokability(isAgentAwaitingSetup(agent) ? { ...agent, status: "idle" } : agent);
if (!invokability.invokable) {
if (caseRequest) return null;
let waitSaved = false;
if (opts.requestedByActorType !== "user" || executionWaitRequestId) {
await writeSkippedRequest("agent.not_invokable", {
const wait = await writeSkippedRequest("agent.not_invokable", {
error: invokability.message,
}, { status: agent.status, reason: invokability.reason });
waitSaved = wait.waitSaved === true;
}
throw conflict(invokability.message, {
status: agent.status,
reason: invokability.reason,
invalidOrgChain: invokability.invalidOrgChain,
...invokability.details,
});
const details = { status: agent.status, reason: invokability.reason,
invalidOrgChain: invokability.invalidOrgChain, ...invokability.details };
throw waitSaved ? new PersistedExecutionWaitRefusal(invokability.message, details)
: conflict(invokability.message, details);
}
const policy = parseHeartbeatPolicy(agent);