mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +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>
89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
import { mkdir, open, readFile, rm } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
const GATE_DIR = "context-comment-gates";
|
|
const HELD_FILE = "held";
|
|
const RELEASE_FILE = "release";
|
|
|
|
export function canonicalDocumentIssueId(url: string | undefined, body: unknown, originalUrl?: string): string | undefined {
|
|
const documentIssueId = (originalUrl ?? url)?.match(/\/api\/issues\/([^/]+)\/documents\/[^/?]+(?:\?|$)/)?.[1];
|
|
if (!documentIssueId) return undefined;
|
|
let parsed: any;
|
|
if (typeof body === "string" || Buffer.isBuffer(body)) {
|
|
try { parsed = JSON.parse(body.toString()); } catch { /* fall through to URL */ }
|
|
}
|
|
if (typeof parsed?.issueId === "string" && parsed.issueId.length > 0) return parsed.issueId;
|
|
return documentIssueId;
|
|
}
|
|
|
|
function privateDir(): string {
|
|
const value = process.env.PAPERCLIP_RUNNER_E2E_PRIVATE_DIR?.trim();
|
|
if (!value) throw new Error("PAPERCLIP_RUNNER_E2E_PRIVATE_DIR is required for the context comment gate");
|
|
return value;
|
|
}
|
|
|
|
function issueDir(issueId: string): string {
|
|
return path.join(privateDir(), GATE_DIR, encodeURIComponent(issueId));
|
|
}
|
|
|
|
async function touchExclusive(file: string): Promise<void> {
|
|
await mkdir(path.dirname(file), { recursive: true });
|
|
try {
|
|
const handle = await open(file, "wx");
|
|
await handle.close();
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
}
|
|
}
|
|
|
|
async function exists(file: string): Promise<boolean> {
|
|
try {
|
|
await readFile(file);
|
|
return true;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function contextCommentGateSelected(executionIds: readonly string[] = JSON.parse(process.env.PAPERCLIP_RUNNER_E2E_EXECUTION_IDS ?? "[]")): boolean {
|
|
return executionIds.some((id) => id.endsWith(".ordered-comment-continuation"));
|
|
}
|
|
|
|
/** Scope a completion-overlap transport hold to the exact source conversation. */
|
|
export async function arm(issueId: string): Promise<void> {
|
|
await touchExclusive(path.join(issueDir(issueId), "armed"));
|
|
}
|
|
|
|
export async function isArmed(issueId: string): Promise<boolean> {
|
|
return exists(path.join(issueDir(issueId), "armed"));
|
|
}
|
|
|
|
export async function waitUntilHeld(issueId: string, deadlineAt: number): Promise<void> {
|
|
const file = path.join(issueDir(issueId), HELD_FILE);
|
|
while (Date.now() < deadlineAt) {
|
|
if (await exists(file)) return;
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
throw new Error(`Timed out waiting for context comment gate for issue ${issueId}`);
|
|
}
|
|
|
|
export async function release(issueId: string): Promise<void> {
|
|
await touchExclusive(path.join(issueDir(issueId), RELEASE_FILE));
|
|
}
|
|
|
|
export async function clear(issueId: string): Promise<void> {
|
|
await rm(issueDir(issueId), { recursive: true, force: true });
|
|
}
|
|
|
|
/** Holds an already-committed initial document write until the flow releases it. */
|
|
export async function holdCommittedDocumentResponse(issueId: string, deadlineAt = Date.now() + 90_000): Promise<void> {
|
|
const directory = issueDir(issueId);
|
|
await touchExclusive(path.join(directory, HELD_FILE));
|
|
while (Date.now() < deadlineAt) {
|
|
if (await exists(path.join(directory, RELEASE_FILE))) return;
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
throw new Error(`Timed out waiting for context comment gate release for issue ${issueId}`);
|
|
}
|