merge: preserve routine projections across retained execution producers

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-30 20:26:14 -05:00
commit 40bd4df17b
201 files changed
+8576 -1103

No files matched your search

@@ -73,7 +73,7 @@ describe("native same-conversation chat attachment reuse", () => {
beforeAll(async () => {
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_CHAT_REUSE_TEST_DATABASE_URL");
if (!external) temporary = await startEmbeddedPostgresTestDatabase("native-chat-reuse-");
db = createDb(external ?? temporary!.connectionString);
db = createDb(external ?? temporary!.connectionString, { maxConnections: 6 });
const storageRoot = await mkdtemp(
path.join(tmpdir(), "paperclip-chat-reuse-"),
);
@@ -1,3 +1,4 @@
import { notifyIssueRecovery } from "../issue-recovery-notifications.js";
import { createHash } from "node:crypto";
import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm";
@@ -1582,7 +1583,9 @@ export async function prepareReusedChatAttachment(input: {
if (!workProduct) {
throw new Error("paperclip_runner_chat_attachment_origin_not_persisted");
}
await input.db
await input.db.transaction(async tx => {
await notifyIssueRecovery(tx, { companyId: input.binding.companyId });
await tx
.update(issueWorkProducts)
.set({
title: input.title,
@@ -1602,6 +1605,7 @@ export async function prepareReusedChatAttachment(input: {
eq(issueWorkProducts.createdByRunId, input.binding.runId),
),
);
});
const comment = await issueService(input.db).addComment(
input.binding.issueId,
`Prepared ${input.title} for this response.`,
@@ -1,3 +1,4 @@
import { notifyStatusCardIssueChange } from "../status-card-notifications.js";
import { notifyExternalAgentSettlement } from "../../modules/external-agents/index.js";
import { pendingNativeRunProjectionCondition } from "./native-run-projection.js";
import { nativeStatusReassessmentSourceIsSuperseded, reassessNativeStatusFromSource } from "./native-status-reassessment.js";
@@ -334,6 +335,7 @@ export async function claimNativeSessionResumptions(input: {
nativePhaseUpdatedAt: now,
updatedAt: now,
}).where(eq(heartbeatRuns.id, row.run.id));
await notifyStatusCardIssueChange(tx, run.companyId);
await tx.update(issues).set({
executionRunId: run.id,
executionAgentNameKey: agent.name.trim().toLowerCase() || null,
@@ -1,3 +1,5 @@
import { notifyAttentionRetentionWork } from "../retention-work-notifications.js";
import { notifyRunDispatch } from "../run-dispatch-notifications.js";
import { randomUUID } from "node:crypto";
import { and, eq } from "drizzle-orm";
import { agentWakeupRequests, heartbeatRuns, type Db } from "@paperclipai/db";
@@ -49,6 +51,8 @@ export async function materializeNativeProviderCapacityRetry(input: {
scheduledRetryAt: dueAt.toISOString(),
executionRetryAccounting: accountingForScheduledRetry(run, NATIVE_PROVIDER_OVERLOADED_CODE, attempt),
};
await notifyAttentionRetentionWork(input.tx, { companyId: input.companyId });
await notifyRunDispatch(input.tx, { companyId: run.companyId });
const [wake] = await input.tx.insert(agentWakeupRequests).values({
companyId: run.companyId, agentId: run.agentId, source: "automation", triggerDetail: "system",
reason: NATIVE_PROVIDER_OVERLOADED_CODE, status: "queued", runId: retryId,
@@ -1,3 +1,5 @@
import { notifyAttentionRetentionWork } from "../retention-work-notifications.js";
import { notifyStatusCardIssueChange } from "../status-card-notifications.js";
import {
agents,
heartbeatRuns,
@@ -240,6 +242,8 @@ export async function claimNativeReviewExecutionLock(db: Db, input: {
if (!run || !["queued", "running"].includes(run.status) || !admitted || !expected
|| admitted.nativeReviewInteractionId !== expected.nativeReviewInteractionId
|| admitted.nativeReviewDecisionId !== expected.nativeReviewDecisionId) return false;
await notifyAttentionRetentionWork(tx, { companyId: input.companyId });
await notifyStatusCardIssueChange(tx, input.companyId);
const claimed = await tx.update(issues).set({
executionRunId: input.runId,
executionAgentNameKey: input.agentNameKey,
@@ -1,3 +1,4 @@
import { resolveExternalTestDatabaseUrl } from "../../__tests__/helpers/external-test-database.js";
import type { PrpStructuredRunResult } from "../../vendor/paperclip-runner/index.js";
import { nativeCompletionFeedback } from "./native-completion-feedback.js";
import { createHash, randomUUID } from "node:crypto";
@@ -61,10 +62,9 @@ describe("native runner file handoff", () => {
const runId = "00000000-0000-4000-8000-000000009104";
beforeAll(async () => {
temporary = await startEmbeddedPostgresTestDatabase(
"native-runner-file-handoff-",
);
db = createDb(temporary.connectionString);
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_NATIVE_HANDOFF_TEST_DATABASE_URL");
if (!external) temporary = await startEmbeddedPostgresTestDatabase("native-runner-file-handoff-");
db = createDb(external ?? temporary!.connectionString, { maxConnections: 6 });
temporaryRoot = await mkdtemp(
path.join(tmpdir(), "paperclip-native-file-handoff-"),
);
@@ -1,3 +1,4 @@
import { notifyIssueRecovery } from "../issue-recovery-notifications.js";
import { createHash, randomUUID } from "node:crypto";
import { execFile } from "node:child_process";
import { constants } from "node:fs";
@@ -1265,7 +1266,9 @@ export async function prepareNativeRunnerFileHandoff(input: {
) {
throw new Error("paperclip_runner_file_handoff_origin_not_persisted");
}
await input.db
await input.db.transaction(async tx => {
await notifyIssueRecovery(tx, { companyId: input.binding.companyId });
await tx
.update(issueWorkProducts)
.set({ title: verified.title, updatedAt: new Date() })
.where(
@@ -1276,6 +1279,7 @@ export async function prepareNativeRunnerFileHandoff(input: {
eq(issueWorkProducts.createdByRunId, input.binding.runId),
),
);
});
const comment = await issueService(input.db).addComment(
input.binding.issueId,
`Prepared ${verified.title} for this response.`,
@@ -4302,6 +4302,17 @@ describe("retained native cleanup activation", () => {
releaseCommit = release;
});
const db = {
execute: async (query: SQL) => {
// Retention hints read the fixture's run/issue relationship inside the
// same cleanup transaction; this fixture has one issue and no ancestors.
const statement = new PgDialect().sqlToQuery(query);
expect(transactionOpen).toBe(true);
expect(statement.params).toContain(run.companyId);
if (statement.sql.includes("from heartbeat_runs")) return [{ id: run.nativeIssueId }];
if (statement.sql.includes("origin_kind")) return [{ id: run.nativeIssueId, originKind: null, originId: null }];
if (statement.sql.includes("with recursive affected")) return [{ issueId: run.nativeIssueId }];
throw new Error(`Unexpected cleanup fixture query: ${statement.sql}`);
},
select: () => ({
from: (table: unknown) => {
const rows =
@@ -1,3 +1,4 @@
import { notifyStatusCardIssueChange } from "../status-card-notifications.js";
import { notifyNativeFinalizationWork } from "../native-finalization-notifications.js";
import { alias } from "drizzle-orm/pg-core";
import { notifyUnsafeWorkspaceExportRecovery, type UnsafeWorkspaceExportScope } from "../unsafe-workspace-export-notifications.js";
@@ -107,6 +108,7 @@ async function recoverCandidate(db: Db, candidate: { runId: string; companyId: s
nativePhase: "result_accepted", nativePhaseUpdatedAt: now,
resultJson: { ...run.resultJson, finalizationPhase: "result_accepted", failureCode: null, originalFailureCode: null, nextAttemptAt: null }, updatedAt: now,
}).where(eq(heartbeatRuns.id, run.id)).returning();
await notifyStatusCardIssueChange(tx, run.companyId);
await tx.update(issues).set({ executionRunId: run.id, updatedAt: now }).where(eq(issues.id, issue.id));
await appendHeartbeatRunEvent(tx as unknown as Db, { companyId: run.companyId, agentId: run.agentId, runId: run.id,
eventType: "workspace_export_omitted", stream: "system", level: "info",
@@ -1,3 +1,4 @@
import { notifyStatusCardIssueChange } from "../status-card-notifications.js";
import { notifyNativeFinalizationWork } from "../native-finalization-notifications.js";
import { notifyEnvironmentCleanup } from "../environment-cleanup-notifications.js";
import { randomUUID } from "node:crypto";
@@ -141,6 +142,7 @@ export async function retryNativeWorkspaceExport(input: {
await tx.update(heartbeatRuns).set({ status: "running", finishedAt: null, error: null, errorCode: null,
nativePhase: "result_accepted", nativePhaseUpdatedAt: now,
resultJson: { ...current.run.resultJson, workspaceExportRetry: retry, finalizationPhase: "result_accepted", failureCode: null, originalFailureCode: null, nextAttemptAt: null }, updatedAt: now }).where(eq(heartbeatRuns.id, current.run.id));
await notifyStatusCardIssueChange(tx, current.run.companyId);
await tx.update(issues).set({ executionRunId: current.run.id, updatedAt: now }).where(eq(issues.id, current.issue.id));
// Reclaim the same lease only for control-plane copyback. The normal
// finalization settlement will stop/retain it again on success or failure.
@@ -131,7 +131,7 @@ describe("native workspace finalization recovery", () => {
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_NATIVE_WORKSPACE_FINALIZER_TEST_DATABASE_URL");
if (!external) temporary = await startEmbeddedPostgresTestDatabase("paperclip-native-workspace-finalizer-");
connectionString = external ?? temporary!.connectionString;
db = createDb(connectionString);
db = createDb(connectionString, { maxConnections: 6 });
await db.insert(companies).values([
{ id: companyId, name: "Native workspace owner", issuePrefix: "NWO" },
@@ -452,7 +452,7 @@ describe("native workspace finalization recovery", () => {
const seeded = await seedRun({ executionWorkspaceId: randomUUID(), title: "Live copyback" });
const live = await withNativeWorkspaceFinalizationOwnership({ db, companyId, runId: seeded.runId }, async (ownership) => {
await ownership.assertHeld();
expect(await resumeNativeWorkspaceFinalization({ db: createDb(connectionString), runId: seeded.runId })).toBeNull();
expect(await resumeNativeWorkspaceFinalization({ db: createDb(connectionString, { maxConnections: 6 }), runId: seeded.runId })).toBeNull();
expect(await db.select().from(workspaceOperations).where(eq(workspaceOperations.heartbeatRunId, seeded.runId))).toEqual([]);
return "copied";
});
@@ -476,7 +476,7 @@ describe("native workspace finalization recovery", () => {
try {
await entered;
await db.execute(sql`select pg_terminate_backend(pid) from pg_locks where locktype = 'advisory' and database = (select oid from pg_database where datname = current_database()) and pid <> pg_backend_pid()`);
const second = await withNativeWorkspaceFinalizationOwnership({ db: createDb(connectionString), companyId, runId: seeded.runId }, async () => "must not run");
const second = await withNativeWorkspaceFinalizationOwnership({ db: createDb(connectionString, { maxConnections: 6 }), companyId, runId: seeded.runId }, async () => "must not run");
expect(second).toEqual({ acquired: false, reason: "busy" });
const [held] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, seeded.runId));
expect(held.runnerProfileJson?.nativeWorkspaceFinalizationOwner).toBeTruthy();
@@ -489,7 +489,7 @@ describe("native workspace finalization recovery", () => {
it("recovers its own joined copyback receipt after the application pool disconnects during cleanup", async () => {
const seeded = await seedRun({ executionWorkspaceId: randomUUID(), title: "Reconnect joined copyback" });
const interruptedDb = createDb(connectionString);
const interruptedDb = createDb(connectionString, { maxConnections: 6 });
let joined = false;
await expect(withNativeWorkspaceFinalizationOwnership({ db: interruptedDb, companyId, runId: seeded.runId }, async () => {
await fs.writeFile(path.join(workspaceRoot, "joined-copyback.txt"), "durable work");
@@ -507,7 +507,7 @@ describe("native workspace finalization recovery", () => {
.toEqual({ acquired: false, reason: "owner_unverified" });
}
await db.update(heartbeatRuns).set({ runnerProfileJson: beforeReconnect.runnerProfileJson }).where(eq(heartbeatRuns.id, seeded.runId));
expect(await withNativeWorkspaceFinalizationOwnership({ db: createDb(connectionString), companyId, runId: seeded.runId }, async () => "resumed"))
expect(await withNativeWorkspaceFinalizationOwnership({ db: createDb(connectionString, { maxConnections: 6 }), companyId, runId: seeded.runId }, async () => "resumed"))
.toEqual({ acquired: true, value: "resumed" });
});
@@ -1,4 +1,6 @@
import { notifyRoutineExecutionStatus } from "../routine-execution-status-notifications.js";
import { notifyAttentionRetentionWork } from "../retention-work-notifications.js";
import { notifyStatusCardIssueChange } from "../status-card-notifications.js";
import { DELIVERY_QUEUES, notifyDeliveryWork } from "../delivery-work-notifications.js";
import { notifyUnsafeWorkspaceExportRecovery } from "../unsafe-workspace-export-notifications.js";
import { and, eq, gt, inArray, ne, notInArray, or, sql } from "drizzle-orm";
@@ -180,6 +182,7 @@ export async function reconstructNativeWorkspace(input: {
...(source.interveningExecutions.length ? { interveningExecutions: source.interveningExecutions } : {}) } };
await markExecutionReconciliation(db, source.action, decision, input.actorId);
const now = new Date();
await notifyAttentionRetentionWork(tx, { companyId: input.companyId });
await notifyUnsafeWorkspaceExportRecovery(tx, { companyId: input.companyId, issueId: input.issueId });
await tx.update(issueRecoveryActions).set({ status: "resolved", outcome: "handed_back", resolvedAt: now, updatedAt: now,
resolutionNote: "workspace_reconstruction_authorized", nextAction: "Continue from saved sources. The original unexported edits are lost; verify the selected attachments before rebuilding. Do not repeat completed external actions or reset the original work limits.",
@@ -189,6 +192,7 @@ export async function reconstructNativeWorkspace(input: {
await notifyDeliveryWork(tx, DELIVERY_QUEUES.issueMonitors);
await notifyDeliveryWork(tx, DELIVERY_QUEUES.agentTimers);
await notifyRoutineExecutionStatus(tx, input.issueId, input.companyId);
await notifyStatusCardIssueChange(tx, input.companyId);
await tx.update(issues).set({ status: "todo", unblockDescriptor: null, blockedTransitionAt: null,
blockedOwnerNotifiedAt: null, updatedAt: now }).where(eq(issues.id, issue.id));
const publication = await persistActivity(db, { companyId: input.companyId, actorType: "user", actorId: input.actorId,
@@ -1,4 +1,6 @@
import { notifyStatusCardIssueChange } from "../status-card-notifications.js";
import { nativeRunKeyScope, nativeSkillTestToolAllowed, assertNativeSkillTestTool } from "./native-skill-test-scope.js";
import { notifyAttentionRetentionWork } from "../retention-work-notifications.js";
import { requiredQualityReviewSchema } from "@paperclipai/shared";
import { priorRecoveryApiReceipt } from "../recovery-effect-receipts.js";
import { DELIVERY_QUEUES, notifyDeliveryWork } from "../delivery-work-notifications.js";
@@ -2067,8 +2069,10 @@ export class PaperclipRunnerToolAuthority {
replayMonitor = summarizeIssueMonitor(target, normalizeIssueExecutionPolicy(target.executionPolicy));
return { taskId: target.id, status: target.status, monitor: replayMonitor };
}
await notifyAttentionRetentionWork(tx, { companyId: this.binding.companyId });
const patch = prepareIssueMonitorUpdate(target, input.monitor, actor);
await notifyDeliveryWork(tx, DELIVERY_QUEUES.issueMonitors);
await notifyStatusCardIssueChange(tx, this.binding.companyId);
const [updated] = await tx.update(issues).set({ ...patch, updatedAt: new Date() })
.where(and(eq(issues.id, target.id), eq(issues.companyId, this.binding.companyId))).returning();
if (!updated) throw notFound("Task not found");
@@ -497,7 +497,7 @@ else if(m.id!==undefined) send({id:m.id,result:{}});
const [agent] = await server.db.select().from(agents).where(eq(agents.id, fixture.agentId));
const context = structuredClone(run.contextSnapshot ?? {});
await prepareHeartbeatTaskContext(server.db, { run, agent,
task: { issueRef: issue, issueContext: null, wakeCommentContext: null, context, taskSession: null },
task: { issueRef: issue, issueContext: null, context, taskSession: null },
policy: { recovering: true, trustPreset: { kind: "standard" }, experimentalInstanceSettings: {} },
services: { issuesSvc: { getAncestors: async () => [] },
getPinnedSkillTestContext: createHeartbeatRunPreparation(server.db).getPinnedSkillTestContext },
@@ -1,3 +1,4 @@
import { notifyNativeStatusWakeWork } from "../deferred-execution-notifications.js";
import { notifyExternalAgentSettlement } from "../../modules/external-agents/index.js";
import { lockCurrentNativeWorkAssessmentEvidence } from "./work-assessments.js";
import { hasNativeWorkspaceFinalizationOwner } from "./native-workspace-finalization-recovery-state.js";
@@ -478,6 +479,7 @@ async function enqueueWake(input: {
.limit(1)
.then((rows) => rows[0] ?? null);
if (existing) return existing.id;
await notifyNativeStatusWakeWork(input.tx, { companyId: input.companyId });
const inserted = await input.tx
.insert(agentWakeupRequests)
.values({
@@ -1,3 +1,4 @@
import { notifyAttentionRetentionWork } from "../retention-work-notifications.js";
import { and, eq, isNull, ne, sql } from "drizzle-orm";
import { completionContracts, heartbeatRuns, issues, issueThreadInteractions, statusDecisionEffects, statusDecisions, workAssessments, type Db } from "@paperclipai/db";
import { hasPublishedGitHubReview } from "../chat-channels/github/review-wait.js";
@@ -70,6 +71,7 @@ export async function supersedeExternalReviewRequestsInTransaction(
publications: ActivityPublication[],
) {
for (const { interaction } of candidates) {
await notifyAttentionRetentionWork(tx, { companyId: binding.companyId });
const reason = "A later published review no longer requests human input.";
const [retired] = await tx.update(issueThreadInteractions).set({ status: "cancelled",
result: { version: 1, outcome: "withdrawn", reason }, resolvedAt: new Date(), updatedAt: new Date(),