import type { AgentImageContent, InboundAgentCommand } from "./client.ts"; import { COMMAND_MARKER_TYPE, commandImageBytes, commandImageDigest, type CommandJournal } from "./command-journal.ts"; export interface CommandPollingDeps { sessionId: string; journal: CommandJournal; appendMarker: (command: InboundAgentCommand) => void; // The public custom-message API queues steer synchronously during a run, // without async user-input preprocessing. Return means handoff, not execution. sendMessage: (message: AgentCommandMessage, options: { deliverAs: "steer"; triggerTurn: true }) => void; canDeliver?: () => boolean; ackCommand: (command: InboundAgentCommand, status: "accepted" | "failed", error?: string) => Promise; onLog?: (message: string) => void; } export interface AgentTextContent { type: "text"; text: string; } export interface AgentImageMessageContent extends AgentImageContent { type: "image"; } export type AgentCommandContent = string | (AgentTextContent | AgentImageMessageContent)[]; export interface AgentCommandMessage { customType: "anon-notify:command"; content: AgentCommandContent; display: true; details: { commandId: string; eventId: string; sessionId: string }; } export function commandLeaseUsable(command: InboundAgentCommand, now = Date.now()): boolean { // Older servers omit expiry. If supplied it must be a valid future deadline. return command.leaseExpiresAt === undefined || Date.parse(command.leaseExpiresAt) > now; } export function formatAgentInstruction(command: InboundAgentCommand): string { return command.instruction; } export function formatAgentMessageContent(command: InboundAgentCommand): AgentCommandContent { if (!command.images?.length) return command.instruction; return [ { type: "text", text: command.instruction }, ...command.images.map((image) => ({ type: "image" as const, data: image.data, mimeType: image.mimeType })), ]; } export async function deliverCommandBatch( commands: readonly InboundAgentCommand[], deps: CommandPollingDeps, ): Promise<{ accepted: number; duplicates: number; failed: number }> { let accepted = 0; let duplicates = 0; let failed = 0; for (const command of commands) { if (deps.canDeliver?.() === false) break; if (!commandLeaseUsable(command)) continue; if (command.sessionId !== deps.sessionId) { failed += 1; await deps.ackCommand(command, "failed", "session_mismatch"); continue; } if (deps.journal.has(command.id)) { duplicates += 1; await deps.ackCommand(command, "accepted"); deps.onLog?.(`duplicate agent command skipped: ${command.id}`); continue; } try { deps.sendMessage({ customType: "anon-notify:command", content: formatAgentMessageContent(command), display: true, details: { commandId: command.id, eventId: command.eventId, sessionId: command.sessionId }, }, { deliverAs: "steer", triggerTurn: true }); } catch (error) { failed += 1; const message = error instanceof Error ? error.message : String(error); await deps.ackCommand(command, "failed", message.slice(0, 500)); deps.onLog?.(`agent command failed before Pi accepted it: ${command.id} - ${message}`); continue; } try { // The journal is durable before ACK, so an ACK failure or lease retry can // acknowledge the same command without injecting a second user message. // The session marker is best-effort observability; the journal remains the // authoritative local dedupe record across reloads. deps.journal.accept(command); try { deps.appendMarker(command); } catch (error) { deps.onLog?.(`agent command marker append failed, journal dedupe retained: ${command.id} - ${error instanceof Error ? error.message : String(error)}`); } } catch (error) { // The Pi call has already been accepted, but local dedupe is not durable. // Do not ACK failure: leaving the lease to expire is safer than telling the // server to retry and knowingly injecting the same command twice. const message = error instanceof Error ? error.message : String(error); failed += 1; deps.onLog?.(`agent command journal failed after Pi acceptance: ${command.id} - ${message}`); throw error; } // ACK failure is deliberately not converted into a failed ACK. The durable // journal makes a later lease retry a duplicate, which can safely ACK the // command without sending the user message again. await deps.ackCommand(command, "accepted"); accepted += 1; deps.onLog?.(`agent command accepted by pi: ${command.id}`); } return { accepted, duplicates, failed }; } export function commandMarker(command: InboundAgentCommand): { customType: typeof COMMAND_MARKER_TYPE; data: Record; } { const digest = commandImageDigest(command); const bytes = commandImageBytes(command); return { customType: COMMAND_MARKER_TYPE, data: { commandId: command.id, eventId: command.eventId, sessionId: command.sessionId, instruction: command.instruction, ...(digest ? { imageDigest: digest } : {}), ...(command.images?.length ? { imageCount: String(command.images.length) } : {}), ...(bytes ? { imageBytes: String(bytes) } : {}), createdAt: command.createdAt, acceptedAt: new Date().toISOString(), }, }; }