import type { AmqMessage } from "../transport/amq-message.js"; export interface FreshSessionProjectionEvidence { readonly messageId: string; readonly sessionId: string; readonly diskVisible: boolean; } export interface FreshSessionCapabilityProbe { probe(): Promise< | { readonly capable: true } | { readonly capable: false; readonly reason: string; readonly dependencies: readonly string[] } >; dispatch( message: AmqMessage, signal?: AbortSignal, canReplaceSession?: (signal?: AbortSignal) => Promise, ): Promise; } export interface FreshSessionReplacementPort { waitForIdle(signal?: AbortSignal): Promise; replace( message: AmqMessage, signal?: AbortSignal, ): Promise<{ readonly cancelled: boolean; readonly projection?: FreshSessionProjectionEvidence; }>; } export class FreshSessionInterlockError extends Error { constructor() { super("worker_runtime_active"); this.name = "FreshSessionInterlockError"; } } /** Full capable path; a real adapter must bind replace() to replacement-context-only session control. */ export class CommandContextFreshSessionCapability implements FreshSessionCapabilityProbe { readonly #port: FreshSessionReplacementPort; constructor(port: FreshSessionReplacementPort) { this.#port = port; } async probe(): Promise<{ readonly capable: true }> { return { capable: true }; } async dispatch( message: AmqMessage, signal?: AbortSignal, canReplaceSession: (signal?: AbortSignal) => Promise = async () => true, ): Promise { if (!await canReplaceSession(signal)) throw new FreshSessionInterlockError(); await this.#port.waitForIdle(signal); throwIfAborted(signal); if (!await canReplaceSession(signal)) throw new FreshSessionInterlockError(); const replaced = await this.#port.replace(message, signal); if (replaced.cancelled) throw new Error("Fresh-session replacement was cancelled"); // diskVisible may legitimately be false here: pi persists a fresh session's file lazily // (only once its first assistant entry arrives), so an in-memory-only projection is a // valid deferred state, not a failure. The journal keeps the record pending with its // target session set until reconciliation later observes the disk-visible projection. if (!replaced.projection || replaced.projection.messageId !== message.header.id) { throw new Error("Fresh-session replacement lacks a matching AMQ projection"); } return replaced.projection; } } export interface FreshSessionPolicy { readonly trustedProject: boolean; readonly allowlisted: (sender: string, project: string) => boolean; readonly confirm?: (message: AmqMessage, signal?: AbortSignal) => Promise; readonly canReplaceSession: (signal?: AbortSignal) => Promise; readonly notifyOnce?: (message: string) => void; } export type FreshSessionDecision = | { readonly kind: "ordinary" } | { readonly kind: "refused"; readonly reason: "not-authorized" | "worker-runtime-active" } | { readonly kind: "pending"; readonly reason: string; readonly action: "/amq ingest" } | { readonly kind: "dispatched"; readonly projection: FreshSessionProjectionEvidence }; /** Serializes privileged session replacement behind explicit policy and capability ports. */ export class FreshSessionDispatcher { readonly #capability: FreshSessionCapabilityProbe; readonly #policy: FreshSessionPolicy; #tail: Promise = Promise.resolve(); #notifiedIncapable = false; constructor(options: { readonly capability: FreshSessionCapabilityProbe; readonly policy: FreshSessionPolicy; }) { this.#capability = options.capability; this.#policy = options.policy; } dispatch(message: AmqMessage, signal?: AbortSignal): Promise { const task = this.#tail.then(() => this.#dispatch(message, signal)); this.#tail = task.then(() => undefined, () => undefined); return task; } async #dispatch(message: AmqMessage, signal?: AbortSignal): Promise { if (!requestsFreshSession(message)) return { kind: "ordinary" }; throwIfAborted(signal); const allowlisted = this.#policy.trustedProject && this.#policy.allowlisted(message.header.from, message.header.fromProject); const confirmed = allowlisted ? true : this.#policy.confirm ? await this.#policy.confirm(message, signal) : false; if (!confirmed) return { kind: "refused", reason: "not-authorized" }; if (!await this.#policy.canReplaceSession(signal)) { return { kind: "refused", reason: "worker-runtime-active" }; } const capability = await this.#capability.probe(); if (!capability.capable) { const reason = `${capability.reason}; requires ${capability.dependencies.join(" and ")}`; if (!this.#notifiedIncapable) { this.#notifiedIncapable = true; this.#policy.notifyOnce?.(`Fresh-session delivery is pending: ${reason}. Use /amq ingest.`); } return { kind: "pending", reason, action: "/amq ingest" }; } try { const projection = await this.#capability.dispatch( message, signal, this.#policy.canReplaceSession, ); if (projection.messageId !== message.header.id) { throw new Error("Fresh-session capability returned an unrelated projection evidence"); } return { kind: "dispatched", projection }; } catch (error) { if (error instanceof FreshSessionInterlockError) { return { kind: "refused", reason: "worker-runtime-active" }; } throw error; } } } export function requestsFreshSession(message: AmqMessage): boolean { const piAmq = message.header.context.pi_amq; return Boolean( piAmq && typeof piAmq === "object" && !Array.isArray(piAmq) && (piAmq as Record).delivery === "fresh-session", ); } function throwIfAborted(signal: AbortSignal | undefined): void { if (!signal?.aborted) return; throw signal.reason instanceof Error ? signal.reason : new Error("Fresh-session dispatch was cancelled"); }