mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 20:34:57 +02:00
Return completed Agent Chat handoffs through a durable outbox and scope each generated update to its supplied tasks. Add recovery, browser delivery, result access, and calibrated quality coverage. Validated with two consecutive ten-case Claude/Codex campaigns, all CI checks, and a 5/5 review. Co-Authored-By: Paperclip <noreply@paperclip.ing>
85 lines
4.3 KiB
TypeScript
85 lines
4.3 KiB
TypeScript
// This entrypoint is used only by isolated Runner E2E instances. Production
|
|
// service code has no test flag, delay, altered prompt, or private test API.
|
|
import { ServerResponse } from "node:http";
|
|
import { PaperclipRunnerToolAuthority } from "../../server/src/services/native-runtime/paperclip-runner-tool-authority.js";
|
|
import { canonicalDocumentIssueId, contextCommentGateSelected, holdCommittedDocumentResponse, isArmed } from "./context-comment-gate.js";
|
|
import { holdInteractionResponse } from "./interaction-response-gate.js";
|
|
|
|
const ids: string[] = JSON.parse(process.env.PAPERCLIP_RUNNER_E2E_EXECUTION_IDS ?? "[]");
|
|
const contextCommentGate = contextCommentGateSelected(ids);
|
|
const completionBusyGate = ids.some(id => id.startsWith("completion-updates.") && id.endsWith(".handoff-completion-busy"));
|
|
if (ids.some((id) => id.endsWith(".accept-while-running"))) {
|
|
const held = new Set<string>();
|
|
const hold = async (value: any) => {
|
|
const interaction = value?.interaction ?? value;
|
|
if (!interaction?.sourceRunId || interaction.status !== "pending" ||
|
|
!["request_confirmation", "request_checkbox_confirmation"].includes(interaction.kind) || held.has(interaction.id)) return;
|
|
held.add(interaction.id);
|
|
await holdInteractionResponse({
|
|
deadlineAt: Date.now() + 90_000,
|
|
loadStatus: async () => {
|
|
const response = await fetch(`http://127.0.0.1:${process.env.PAPERCLIP_RUNNER_E2E_PORT}/api/issues/${interaction.issueId}/interactions`);
|
|
if (!response.ok) throw new Error(`Approval barrier read failed: ${response.status}`);
|
|
const rows = await response.json() as Array<{ id: string; status: string }>;
|
|
const row = rows.find((candidate) => candidate.id === interaction.id);
|
|
if (!row) throw new Error("Approval barrier lost its committed card");
|
|
return row.status;
|
|
},
|
|
});
|
|
};
|
|
const execute = PaperclipRunnerToolAuthority.prototype.execute;
|
|
PaperclipRunnerToolAuthority.prototype.execute = async function (...args) {
|
|
const result = await execute.apply(this, args);
|
|
if (args[0].tool === "request_human_input") await hold(result);
|
|
return result;
|
|
};
|
|
const end = ServerResponse.prototype.end;
|
|
ServerResponse.prototype.end = function (this: ServerResponse, ...args: any[]) {
|
|
const body = args[0];
|
|
let interaction: any;
|
|
if (this.req.method === "POST" && /\/interactions(?:\?|$)/.test(this.req.url ?? "") &&
|
|
this.statusCode >= 200 && this.statusCode < 300 && (typeof body === "string" || Buffer.isBuffer(body))) {
|
|
try { interaction = JSON.parse(body.toString()); } catch { /* non-JSON response */ }
|
|
}
|
|
if (interaction?.sourceRunId && ["request_confirmation", "request_checkbox_confirmation"].includes(interaction.kind)) {
|
|
void hold(interaction).then(() => Reflect.apply(end, this, args), (error) => this.destroy(error));
|
|
return this;
|
|
}
|
|
return Reflect.apply(end, this, args);
|
|
} as typeof end;
|
|
}
|
|
if (contextCommentGate || completionBusyGate) {
|
|
const heldIssues = new Set<string>();
|
|
const holdFirstDocument = async (issueId: string) => {
|
|
if (heldIssues.has(issueId) || (!contextCommentGate && !await isArmed(issueId))) return;
|
|
heldIssues.add(issueId);
|
|
await holdCommittedDocumentResponse(issueId, Date.now() + (completionBusyGate ? 300_000 : 90_000));
|
|
};
|
|
const execute = PaperclipRunnerToolAuthority.prototype.execute;
|
|
PaperclipRunnerToolAuthority.prototype.execute = async function (...args) {
|
|
const result = await execute.apply(this, args);
|
|
if (args[0].tool === "write_document") {
|
|
const issueId = this.binding?.issueId;
|
|
if (typeof issueId === "string") await holdFirstDocument(issueId);
|
|
}
|
|
return result;
|
|
};
|
|
const end = ServerResponse.prototype.end;
|
|
ServerResponse.prototype.end = function (this: ServerResponse, ...args: any[]) {
|
|
const body = args[0];
|
|
const request = this.req as typeof this.req & { originalUrl?: string };
|
|
const issueId = this.req.method === "PUT"
|
|
? canonicalDocumentIssueId(request.url, body, request.originalUrl)
|
|
: undefined;
|
|
if (issueId && this.statusCode >= 200 && this.statusCode < 300) {
|
|
void holdFirstDocument(decodeURIComponent(issueId)).then(
|
|
() => Reflect.apply(end, this, args),
|
|
(error) => this.destroy(error),
|
|
);
|
|
return this;
|
|
}
|
|
return Reflect.apply(end, this, args);
|
|
} as typeof end;
|
|
}
|
|
await import("../../cli/src/index.js");
|