Files
EJClaw/src/ipc-outbound-delivery.ts

194 lines
5.3 KiB
TypeScript

import { createProducedWorkItem } from './db.js';
import type { WorkItem } from './db/work-items.js';
import { resolveRuntimeAttachmentBaseDirs } from './attachment-base-dirs.js';
import { deliverOpenWorkItem } from './message-runtime-delivery.js';
import { parseVisibleVerdict } from './paired-verdict.js';
import { resolveChannelForDeliveryRole } from './router.js';
import { normalizePairedRoomRole } from './types.js';
import type {
Channel,
OutboundAttachment,
PairedRoomRole,
RegisteredGroup,
} from './types.js';
import { logger } from './logger.js';
type IpcDeliveryLog = Pick<typeof logger, 'info' | 'warn' | 'error'>;
interface IpcOutboundQueue {
hasRecordedDirectTerminalDeliveryForRun?(
groupJid: string,
runId: string,
senderRole?: string | null,
): boolean;
noteDirectTerminalDelivery?(
groupJid: string,
senderRole?: string | null,
text?: string | null,
): void;
}
export interface IpcOutboundDeliveryArgs {
jid: string;
text: string;
senderRole?: string;
runId?: string;
attachments?: OutboundAttachment[];
}
export interface IpcOutboundDeliveryDeps {
channels: Channel[];
roomBindings: () => Record<string, RegisteredGroup>;
queue: IpcOutboundQueue;
log?: IpcDeliveryLog;
createWorkItem?: typeof createProducedWorkItem;
deliverWorkItem?: typeof deliverOpenWorkItem;
}
export interface CanonicalOutboundDeliveryArgs {
jid: string;
text: string;
deliveryRole?: PairedRoomRole;
attachments?: OutboundAttachment[];
}
export interface CanonicalOutboundDeliveryDeps {
channels: Channel[];
roomBindings: () => Record<string, RegisteredGroup>;
log?: IpcDeliveryLog;
createWorkItem?: typeof createProducedWorkItem;
deliverWorkItem?: typeof deliverOpenWorkItem;
}
export type CanonicalOutboundDeliveryResult = 'delivered' | 'queued_retry';
export type IpcOutboundDeliveryResult =
| 'delivered'
| 'queued_retry'
| 'skipped_recorded_terminal';
function isTerminalStatusMessage(text: string): boolean {
return parseVisibleVerdict(text) !== 'continue';
}
export async function deliverCanonicalOutboundMessage(
args: CanonicalOutboundDeliveryArgs,
deps: CanonicalOutboundDeliveryDeps,
): Promise<CanonicalOutboundDeliveryResult> {
const log = deps.log ?? logger;
const group = deps.roomBindings()[args.jid];
if (!group) {
throw new Error(`No registered room binding for outbound JID: ${args.jid}`);
}
const route = resolveChannelForDeliveryRole(
deps.channels,
args.jid,
args.deliveryRole,
);
if (!route.channel) throw new Error(`No channel for JID: ${args.jid}`);
log.info(
{
transition: 'outbound:route',
chatJid: args.jid,
deliveryRole: args.deliveryRole ?? null,
requestedRoleChannel: route.requestedRoleChannelName,
selectedChannel: route.selectedChannelName,
usedRoleChannel: route.usedRoleChannel,
fallbackUsed: route.fallbackUsed,
},
'Routed outbound message to canonical work item delivery',
);
const createWorkItem = deps.createWorkItem ?? createProducedWorkItem;
const workItem = createWorkItem({
group_folder: group.folder,
chat_jid: args.jid,
agent_type: group.agentType ?? 'claude-code',
delivery_role: args.deliveryRole ?? null,
start_seq: null,
end_seq: null,
result_payload: args.text,
attachments: args.attachments,
});
const deliverWorkItem = deps.deliverWorkItem ?? deliverOpenWorkItem;
const delivered = await deliverWorkItem({
channel: route.channel,
item: workItem as WorkItem,
log,
attachmentBaseDirs: resolveRuntimeAttachmentBaseDirs(group),
isDuplicateOfLastBotFinal: () => false,
openContinuation: () => {},
});
return delivered ? 'delivered' : 'queued_retry';
}
export async function deliverIpcOutboundMessage(
args: IpcOutboundDeliveryArgs,
deps: IpcOutboundDeliveryDeps,
): Promise<IpcOutboundDeliveryResult> {
const log = deps.log ?? logger;
const deliveryRole = normalizePairedRoomRole(args.senderRole);
if (
args.runId &&
(deliveryRole === 'reviewer' || deliveryRole === 'arbiter') &&
deps.queue.hasRecordedDirectTerminalDeliveryForRun?.(
args.jid,
args.runId,
deliveryRole,
)
) {
log.info(
{
transition: 'ipc:skip-post-terminal',
chatJid: args.jid,
senderRole: deliveryRole,
runId: args.runId,
},
'Skipped IPC relay message because the run already emitted a direct terminal verdict',
);
return 'skipped_recorded_terminal';
}
log.info(
{
transition: 'ipc:route',
chatJid: args.jid,
runId: args.runId ?? null,
senderRole: deliveryRole ?? null,
},
'IPC relay routed message to canonical work item delivery',
);
const result = await deliverCanonicalOutboundMessage(
{
jid: args.jid,
text: args.text,
deliveryRole,
attachments: args.attachments,
},
{
channels: deps.channels,
roomBindings: deps.roomBindings,
log,
createWorkItem: deps.createWorkItem,
deliverWorkItem: deps.deliverWorkItem,
},
);
if (result !== 'delivered') {
return result;
}
if (
(deliveryRole === 'reviewer' || deliveryRole === 'arbiter') &&
isTerminalStatusMessage(args.text)
) {
deps.queue.noteDirectTerminalDelivery?.(args.jid, deliveryRole, args.text);
}
return 'delivered';
}