src/runner/publication.ts
Rama main · réplica de lectura del repositorio original.
import { runNoticeBody, type RunNotice } from './run-notice.ts';
import type { ScmAdapter } from '../scm/types.ts';
import type { Trace } from '../harness/trace.ts';
import { safeError } from './outbound.ts';
import {
deferDelivery,
deliverImmediately,
enqueueAndDeliverComment,
enqueueCommentDelivery,
type DeliveryAttempt,
type OutboundConnection,
} from './outbound.ts';
import { closeoutMarker } from './closeout-publication.ts';
export type TaskPublication = DeliveryAttempt['publication'];
function cleanString(trace: Trace, value: string) {
const cleaned: unknown = JSON.parse(trace.clean(value));
if (typeof cleaned !== 'string') throw new Error('Task answer did not remain a string after redaction');
return cleaned;
}
export interface TaskPublicationInput {
root: string;
repo: string;
prNumber: number;
runId: string;
status: 'custom_completed' | 'conflict_completed' | 'ci_completed' | 'repair_completed';
body: string;
workspaceNotice?: string;
headSha: string;
projectId: string;
mentions: string[];
botLogin: string;
adapter: ScmAdapter;
trace: Trace;
source?: Record<string, string | number | null | undefined>;
approvalPlan?: {
plan_id: string;
plan_revision: number;
body_sha256: string;
source_comment_id: number;
};
guard?: () => Promise<void>;
}
/**
* Publish an opaque task answer after its durable writeback has settled.
*
* This boundary owns only communication artifacts and the outbox. It does not decide task status,
* mutate run state, or retry an Agent. The caller can therefore recover a queued publication by its
* semantic key without replaying the task or pushing another candidate.
*/
export async function publishTaskAnswer(input: TaskPublicationInput) {
await input.guard?.();
const answer = cleanString(input.trace, `${input.workspaceNotice ?? ''}${input.body}`);
input.trace.save('delivery.json', {
status: input.status,
head_sha: input.headSha,
body: answer,
...(input.approvalPlan ? { approval_plan: input.approvalPlan } : {}),
});
const publication = (await enqueueAndDeliverComment({
root: input.root,
repo: input.repo,
prNumber: input.prNumber,
purpose: 'delivery_report',
semanticKey: `run:${input.runId}:delivery:${input.status}`,
body: answer,
mentions: input.mentions,
botLogin: input.botLogin,
source: input.source ?? { project_id: input.projectId, run_id: input.runId, status: input.status },
}, { adapter: input.adapter, botLogin: input.botLogin })).publication;
input.trace.save('delivery-publication.json', publication);
await input.guard?.();
return { answer, publication };
}
export interface RunNoticePublicationInput {
root: string;
repo: string;
prNumber: number;
projectId: string;
trace: Trace;
notice: RunNotice;
botLogin?: string;
adapter?: ScmAdapter;
resolveConnection?: () => Promise<OutboundConnection>;
}
function notificationArtifact(publication: TaskPublication) {
if (publication.status === 'published') return publication;
if (publication.status === 'blocked') {
return {
status: 'notification_failed',
http_status: publication.last_error?.status ?? null,
last_error: publication.last_error,
};
}
return { ...publication, status: 'notification_pending' };
}
/**
* Persist and deliver a previously prepared run notice. Installation lookup is deliberately a
* delivery concern: if it fails, the already-enqueued body remains recoverable by the scheduler.
*/
export async function publishRunNotice(input: RunNoticePublicationInput) {
try {
const stored = await enqueueCommentDelivery({
root: input.root,
repo: input.repo,
prNumber: input.prNumber,
purpose: 'run_notice',
semanticKey: `run-notice:${input.notice.run_id}`,
body: runNoticeBody(input.notice),
mentions: input.notice.mentions,
botLogin: input.botLogin,
legacyMarkers: ['budget_exhausted', 'stopped'].includes(input.notice.status)
? [closeoutMarker(input.notice.run_id)] : undefined,
source: { project_id: input.projectId, run_id: input.notice.run_id, status: input.notice.status },
});
let publication: TaskPublication;
try {
const connection = input.adapter
? { adapter: input.adapter, botLogin: input.botLogin }
: await input.resolveConnection?.();
if (!connection) throw new Error('SCM delivery connection is unavailable');
publication = (await deliverImmediately(input.root, stored, connection)).publication;
} catch (error) {
publication = (await deferDelivery(input.root, stored, error)).publication;
}
input.trace.emit('run_notice_published', publication);
input.trace.save('notification.json', notificationArtifact(publication));
return publication;
} catch (error) {
const failure = {
status: 'notification_failed',
http_status: (error as { status?: number }).status ?? null,
last_error: safeError(error),
};
input.trace.emit('run_notice_failed', failure);
input.trace.save('notification.json', failure);
return failure;
}
}