/** * Public Notary Role run: admit source-run locator → shared post-admission coordinator * → settle Terminal result (#448 / #517). Explicit new has zero caller * prompt/attachment; explicit resume accepts the shared optional caller message. * Lifecycle is the shared post-admission seam; this module keeps only Notary adapters. * #637 / #747: same-parent (--source-run) re-summons resume the seat's previous run. */ import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts"; import { NotarySourceRunError, resolveNotarySourceRunLocator, } from "../notary-source-run.ts"; import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.ts"; import { CliUsageError } from "./cli-errors.ts"; import { admitNotaryInvocation, buildNotaryTransportPrompt, type AdmittedNotaryInvocation, type ParseNotaryArgvResult, } from "./invocation.ts"; import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts"; import { prepareSummonsResumeMaterials, runPostAdmissionOneShot, type PostAdmissionAdapters, type PostAdmissionEnv, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.ts"; import { loadResumableNotaryRun, markRunAdmitted, type PublicResumeRequest, type SameTicketSummonsMaterials, } from "./run-lifecycle.ts"; import { presentStructuralRejection, trySettleNotaryTerminalResult, } from "./settlement.ts"; import type { CliIo } from "./cli-io.ts"; import type { TerminalResult } from "./terminal.ts"; import { projectRoleTurnRequest, type RoleTurnRequestProjectionOptions, } from "./turn-request.ts"; export type NotaryRunEnv = PostAdmissionEnv & { principalAuthority: DurablePrincipalAuthority; createRunId?: () => string; /** * #753 nested gate re-ask: plain-language instruction on same-ticket resume. * Only set by summonPublicRole when the prior officer reply was not three-state. * Never a public CLI argv — external callers still have zero prompt. */ reviewReask?: string; /** * #879 gate summons: verbatim parent-submission body for officer dialogue content. * Rides summons.instruction / first-mint prompt when reviewReask is absent. * Binding pointer stays --source-run (independent machine material). */ gateReviewInstruction?: string; }; /** Project admitted invocation onto the host-neutral turn request. */ export function buildNotaryTurnRequest( admitted: AdmittedNotaryInvocation, options: RoleTurnRequestProjectionOptions, ): RoleTurnRequest { return projectRoleTurnRequest( admitted, { activation: { role: "notary" as const, sourceRun: admitted.sourceRunPath, // Ticket from source-run admitted form rides activation → flag → role read surface (#635). ...(admitted.ticketNumber === undefined ? {} : { ticketNumber: admitted.ticketNumber }), }, }, options, ); } export async function runPublicNotary( argv: readonly string[], env: NotaryRunEnv, io: CliIo, parseNotaryArgv: (args: readonly string[]) => ParseNotaryArgvResult, ): Promise<{ exitCode: number; admitted?: AdmittedNotaryInvocation; terminal?: TerminalResult; }> { let parsed: ParseNotaryArgvResult; try { parsed = parseNotaryArgv(argv); } catch (error) { if (error instanceof CliUsageError) { presentStructuralRejection(error, io); return { exitCode: 2 }; } throw error; } // #637: same ticket → resume prior notary run with this summons' source-run pointer. // Source-run structural failures stay usage rejections; lookup/resume failures surface // (no bare catch→fresh). Only true absence of a prior run mints new. const projectRoot = parsed.project ?? env.cwd; let source; try { source = await resolveNotarySourceRunLocator({ projectRoot, sourceRun: parsed.sourceRun, home: env.home, }); } catch (error) { if (error instanceof NotarySourceRunError) { presentStructuralRejection(new CliUsageError(error.message, { cause: error }), io); return { exitCode: 2 }; } throw error; } // #747: officer resume key is this parent source-run path (not ticket number). // #753/#879: gate re-ask and verbatim submission body share summons.instruction // (reask wins when both present; no parallel stack). Binding stays sourceRunPath. { const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction; const summons: SameTicketSummonsMaterials = { sourceRunPath: source.runDirectory, sourceRun: source, ...(resumeInstruction === undefined ? {} : { instruction: resumeInstruction, instructionEmpty: false }), }; const resumed = await tryResumeSameTicketSeatRun({ home: env.home, projectRoot, role: "notary", parentRunPath: source.runDirectory, freshSummons: env.freshSummons, summons, resume: (runId, materials) => runPublicNotaryResume( { runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io, ), }); if (resumed !== undefined) return resumed; } let admitted: AdmittedNotaryInvocation; try { admitted = await admitNotaryInvocation({ home: env.home, principalAuthority: env.principalAuthority, cwd: env.cwd, sourceRun: parsed.sourceRun, ...(parsed.project === undefined ? {} : { project: parsed.project }), ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }), ...(env.model === undefined ? {} : { model: env.model }), ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }), }); } catch (error) { if (error instanceof CliUsageError) { presentStructuralRejection(error, io); return { exitCode: 2 }; } throw error; } await markRunAdmitted(admitted, env.principalAuthority); const engineMaterial = engineSessionMaterialFromOptions({ ...pickEngineAxis(env), packageRoot: env.packageRoot, }); const turnRequest = buildNotaryTurnRequest(admitted, { packageRoot: env.packageRoot, home: env.home, agentDir: env.agentDir, ...(env.model === undefined ? {} : { model: env.model }), ...pickEngineAxis(env), ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }), ...(env.correlationId === undefined || env.correlationId.trim() === "" ? {} : { correlationId: env.correlationId }), continuation: { kind: "initial", // #879: gate path first mint carries parent payload as dialogue content; // external zero-prompt kickoff unchanged when no body/reask. prompt: (env.reviewReask ?? env.gateReviewInstruction) ?? buildNotaryTransportPrompt(admitted, engineMaterial), }, }); return await runPostAdmissionOneShot({ admitted, env, io, request: turnRequest, adapters: notaryAdapters(), ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }), }); } function notaryAdapters(): PostAdmissionAdapters { return { trySettle: (admitted, authority, scope) => trySettleNotaryTerminalResult(admitted, authority, scope), // Accepted receipts and failure terminals both present via shared path. shouldPresentSettled: () => true, }; } /** * Resume a previously admitted Notary run (#633 / #637). Manual resume restores * source-run from the durable admitted request. Same-ticket re-summons deliver * this turn's source-run activation pointer while reopening the same session. */ export async function runPublicNotaryResume( request: PublicResumeRequest, env: NotaryRunEnv, io: CliIo, ): Promise<{ exitCode: number; admitted?: AdmittedNotaryInvocation; terminal?: TerminalResult; }> { return await runPostAdmissionSeatResume({ request, env, io, load: async (effective) => { const loaded = await loadResumableNotaryRun( env.home, effective.runId, env.principalAuthority, ); const summons = effective.summons; if ( summons?.sourceRunPath !== undefined && summons.sourceRun !== undefined ) { return { admitted: { ...loaded.admitted, sourceRunPath: summons.sourceRunPath, sourceRun: summons.sourceRun, }, }; } return loaded; }, buildTurnRequest: async (admitted, effective) => { // #755: review continuation is plain dialogue / resume envelope — no //「重新读」candidate line, no engine handbook packaging. // #753: non-three-state re-ask rides as summons.instruction (人话重问). const summonsPrepared = effective.summons === undefined ? undefined : await prepareSummonsResumeMaterials( admitted.runDirectory, effective.summons, ); return buildNotaryTurnRequest( admitted, resumeTurnRequestProjectionOptions( admitted, effective, env, summonsPrepared, ), ); }, adapters: notaryAdapters(), ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }), }); }