Merge pull request #88 from phj1081/codex/owner/ejclaw
Route ordinary outbound through canonical delivery
This commit is contained in:
@@ -3,7 +3,6 @@ import { describe, expect, it, vi } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
editFormattedTrackedChannelMessage,
|
editFormattedTrackedChannelMessage,
|
||||||
isTerminalStatusMessage,
|
isTerminalStatusMessage,
|
||||||
sendFormattedChannelMessage,
|
|
||||||
sendFormattedTrackedChannelMessage,
|
sendFormattedTrackedChannelMessage,
|
||||||
} from './index.js';
|
} from './index.js';
|
||||||
import { composeDashboardContent } from './dashboard-render.js';
|
import { composeDashboardContent } from './dashboard-render.js';
|
||||||
@@ -81,12 +80,6 @@ describe('index scheduler messaging helpers', () => {
|
|||||||
'msg-123',
|
'msg-123',
|
||||||
'<internal>only hidden</internal>',
|
'<internal>only hidden</internal>',
|
||||||
);
|
);
|
||||||
await sendFormattedChannelMessage(
|
|
||||||
[channel],
|
|
||||||
'dc:test',
|
|
||||||
'<internal>only hidden</internal>',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(trackedResult).toBeNull();
|
expect(trackedResult).toBeNull();
|
||||||
expect(sendAndTrack).not.toHaveBeenCalled();
|
expect(sendAndTrack).not.toHaveBeenCalled();
|
||||||
expect(editMessage).not.toHaveBeenCalled();
|
expect(editMessage).not.toHaveBeenCalled();
|
||||||
|
|||||||
63
src/index.ts
63
src/index.ts
@@ -30,10 +30,12 @@ import { composeDashboardContent } from './dashboard-render.js';
|
|||||||
import { GroupQueue } from './group-queue.js';
|
import { GroupQueue } from './group-queue.js';
|
||||||
import { resolveGroupIpcPath } from './group-folder.js';
|
import { resolveGroupIpcPath } from './group-folder.js';
|
||||||
import { startIpcWatcher } from './ipc.js';
|
import { startIpcWatcher } from './ipc.js';
|
||||||
import { deliverIpcOutboundMessage } from './ipc-outbound-delivery.js';
|
import {
|
||||||
|
deliverCanonicalOutboundMessage,
|
||||||
|
deliverIpcOutboundMessage,
|
||||||
|
} from './ipc-outbound-delivery.js';
|
||||||
import {
|
import {
|
||||||
findChannel,
|
findChannel,
|
||||||
findChannelByName,
|
|
||||||
formatOutbound,
|
formatOutbound,
|
||||||
normalizeMessageForDedupe,
|
normalizeMessageForDedupe,
|
||||||
} from './router.js';
|
} from './router.js';
|
||||||
@@ -90,20 +92,6 @@ import { FAILOVER_MIN_DURATION_MS } from './config.js';
|
|||||||
|
|
||||||
// Token rotation is initialized lazily on first use or at startup below
|
// Token rotation is initialized lazily on first use or at startup below
|
||||||
|
|
||||||
export async function sendFormattedChannelMessage(
|
|
||||||
channels: Channel[],
|
|
||||||
jid: string,
|
|
||||||
rawText: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const channel = findChannel(channels, jid);
|
|
||||||
if (!channel) {
|
|
||||||
logger.warn({ jid }, 'No channel owns JID, cannot send message');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const text = formatOutbound(rawText);
|
|
||||||
if (text) await channel.sendMessage(jid, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendFormattedTrackedChannelMessage(
|
export async function sendFormattedTrackedChannelMessage(
|
||||||
channels: Channel[],
|
channels: Channel[],
|
||||||
jid: string,
|
jid: string,
|
||||||
@@ -156,6 +144,23 @@ const runtime = createMessageRuntime({
|
|||||||
clearSession: runtimeState.clearSession,
|
clearSession: runtimeState.clearSession,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function deliverFormattedCanonicalMessage(
|
||||||
|
jid: string,
|
||||||
|
rawText: string,
|
||||||
|
deliveryRole?: 'owner' | 'reviewer' | 'arbiter',
|
||||||
|
): Promise<void> {
|
||||||
|
const text = formatOutbound(rawText);
|
||||||
|
if (!text) return;
|
||||||
|
await deliverCanonicalOutboundMessage(
|
||||||
|
{ jid, text, deliveryRole },
|
||||||
|
{
|
||||||
|
channels,
|
||||||
|
roomBindings: runtimeState.getRoomBindings,
|
||||||
|
log: logger,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get available groups list for the agent.
|
* Get available groups list for the agent.
|
||||||
* Returns groups ordered by most recent activity.
|
* Returns groups ordered by most recent activity.
|
||||||
@@ -192,8 +197,7 @@ async function announceRestartRecovery(
|
|||||||
return explicitContext;
|
return explicitContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendFormattedChannelMessage(
|
await deliverFormattedCanonicalMessage(
|
||||||
channels,
|
|
||||||
explicitContext.chatJid,
|
explicitContext.chatJid,
|
||||||
buildRestartAnnouncement(explicitContext),
|
buildRestartAnnouncement(explicitContext),
|
||||||
);
|
);
|
||||||
@@ -207,8 +211,7 @@ async function announceRestartRecovery(
|
|||||||
if (hasRecentRestartAnnouncement(interrupted.chatJid, dedupeSince)) {
|
if (hasRecentRestartAnnouncement(interrupted.chatJid, dedupeSince)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await sendFormattedChannelMessage(
|
await deliverFormattedCanonicalMessage(
|
||||||
channels,
|
|
||||||
interrupted.chatJid,
|
interrupted.chatJid,
|
||||||
buildInterruptedRestartAnnouncement(interrupted),
|
buildInterruptedRestartAnnouncement(interrupted),
|
||||||
);
|
);
|
||||||
@@ -230,8 +233,7 @@ async function announceRestartRecovery(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendFormattedChannelMessage(
|
await deliverFormattedCanonicalMessage(
|
||||||
channels,
|
|
||||||
inferred.chatJid,
|
inferred.chatJid,
|
||||||
inferred.lines.join('\n'),
|
inferred.lines.join('\n'),
|
||||||
);
|
);
|
||||||
@@ -360,13 +362,6 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start subsystems (independently of connection handler)
|
// Start subsystems (independently of connection handler)
|
||||||
// Paired-room scheduler output goes through the reviewer bot slot.
|
|
||||||
const reviewerChannelName = 'discord-review';
|
|
||||||
const reviewerChannelForCron = findChannelByName(
|
|
||||||
channels,
|
|
||||||
reviewerChannelName,
|
|
||||||
);
|
|
||||||
|
|
||||||
startSchedulerLoop({
|
startSchedulerLoop({
|
||||||
roomBindings: runtimeState.getRoomBindings,
|
roomBindings: runtimeState.getRoomBindings,
|
||||||
getSessions: runtimeState.getSessions,
|
getSessions: runtimeState.getSessions,
|
||||||
@@ -374,13 +369,9 @@ async function main(): Promise<void> {
|
|||||||
onProcess: (groupJid, proc, processName, ipcDir) =>
|
onProcess: (groupJid, proc, processName, ipcDir) =>
|
||||||
queue.registerProcess(groupJid, proc, processName, ipcDir),
|
queue.registerProcess(groupJid, proc, processName, ipcDir),
|
||||||
sendMessage: (jid, rawText) =>
|
sendMessage: (jid, rawText) =>
|
||||||
sendFormattedChannelMessage(channels, jid, rawText),
|
deliverFormattedCanonicalMessage(jid, rawText),
|
||||||
sendMessageViaReviewerBot: reviewerChannelForCron
|
sendMessageViaReviewerBot: (jid, rawText) =>
|
||||||
? async (jid, rawText) => {
|
deliverFormattedCanonicalMessage(jid, rawText, 'reviewer'),
|
||||||
const text = formatOutbound(rawText);
|
|
||||||
if (text) await reviewerChannelForCron.sendMessage(jid, text);
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
sendTrackedMessage: (jid, rawText) =>
|
sendTrackedMessage: (jid, rawText) =>
|
||||||
sendFormattedTrackedChannelMessage(channels, jid, rawText),
|
sendFormattedTrackedChannelMessage(channels, jid, rawText),
|
||||||
editTrackedMessage: (jid, messageId, rawText) =>
|
editTrackedMessage: (jid, messageId, rawText) =>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { deliverIpcOutboundMessage } from './ipc-outbound-delivery.js';
|
import {
|
||||||
|
deliverCanonicalOutboundMessage,
|
||||||
|
deliverIpcOutboundMessage,
|
||||||
|
} from './ipc-outbound-delivery.js';
|
||||||
import type { WorkItem } from './db/work-items.js';
|
import type { WorkItem } from './db/work-items.js';
|
||||||
import type { Channel, RegisteredGroup } from './types.js';
|
import type { Channel, RegisteredGroup } from './types.js';
|
||||||
|
|
||||||
@@ -56,6 +59,47 @@ function makeWorkItem(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('deliverIpcOutboundMessage', () => {
|
describe('deliverIpcOutboundMessage', () => {
|
||||||
|
it('funnels ordinary outbound through canonical work item delivery', async () => {
|
||||||
|
const channel = makeChannel();
|
||||||
|
const createdItem = makeWorkItem({
|
||||||
|
chat_jid: 'dc:room',
|
||||||
|
group_folder: 'room-folder',
|
||||||
|
result_payload: '일반 안내',
|
||||||
|
});
|
||||||
|
const createWorkItem = vi.fn(() => createdItem);
|
||||||
|
const deliverWorkItem = vi.fn(async () => true);
|
||||||
|
|
||||||
|
const result = await deliverCanonicalOutboundMessage(
|
||||||
|
{ jid: 'dc:room', text: '일반 안내' },
|
||||||
|
{
|
||||||
|
channels: [channel],
|
||||||
|
roomBindings: () => ({ 'dc:room': makeGroup() }),
|
||||||
|
createWorkItem,
|
||||||
|
deliverWorkItem,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe('delivered');
|
||||||
|
expect(createWorkItem).toHaveBeenCalledWith({
|
||||||
|
group_folder: 'room-folder',
|
||||||
|
chat_jid: 'dc:room',
|
||||||
|
agent_type: 'codex',
|
||||||
|
delivery_role: null,
|
||||||
|
start_seq: null,
|
||||||
|
end_seq: null,
|
||||||
|
result_payload: '일반 안내',
|
||||||
|
attachments: undefined,
|
||||||
|
});
|
||||||
|
expect(deliverWorkItem).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
channel,
|
||||||
|
item: createdItem,
|
||||||
|
attachmentBaseDirs: ['/repo'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('funnels IPC outbound through canonical work item delivery', async () => {
|
it('funnels IPC outbound through canonical work item delivery', async () => {
|
||||||
const ownerChannel = makeChannel();
|
const ownerChannel = makeChannel();
|
||||||
const reviewerChannel = makeChannel('discord-review', false);
|
const reviewerChannel = makeChannel('discord-review', false);
|
||||||
@@ -155,7 +199,7 @@ describe('deliverIpcOutboundMessage', () => {
|
|||||||
deliverWorkItem,
|
deliverWorkItem,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).rejects.toThrow('No registered room binding for IPC outbound JID');
|
).rejects.toThrow('No registered room binding for outbound JID');
|
||||||
|
|
||||||
expect(createWorkItem).not.toHaveBeenCalled();
|
expect(createWorkItem).not.toHaveBeenCalled();
|
||||||
expect(deliverWorkItem).not.toHaveBeenCalled();
|
expect(deliverWorkItem).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -43,6 +43,23 @@ export interface IpcOutboundDeliveryDeps {
|
|||||||
deliverWorkItem?: typeof deliverOpenWorkItem;
|
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 =
|
export type IpcOutboundDeliveryResult =
|
||||||
| 'delivered'
|
| 'delivered'
|
||||||
| 'queued_retry'
|
| 'queued_retry'
|
||||||
@@ -65,6 +82,61 @@ function isTerminalStatusMessage(text: string): boolean {
|
|||||||
return parseVisibleVerdict(text) !== 'continue';
|
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: group.workDir ? [group.workDir] : undefined,
|
||||||
|
isDuplicateOfLastBotFinal: () => false,
|
||||||
|
openContinuation: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
return delivered ? 'delivered' : 'queued_retry';
|
||||||
|
}
|
||||||
|
|
||||||
export async function deliverIpcOutboundMessage(
|
export async function deliverIpcOutboundMessage(
|
||||||
args: IpcOutboundDeliveryArgs,
|
args: IpcOutboundDeliveryArgs,
|
||||||
deps: IpcOutboundDeliveryDeps,
|
deps: IpcOutboundDeliveryDeps,
|
||||||
@@ -92,58 +164,34 @@ export async function deliverIpcOutboundMessage(
|
|||||||
return 'skipped_recorded_terminal';
|
return 'skipped_recorded_terminal';
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = deps.roomBindings()[args.jid];
|
|
||||||
if (!group) {
|
|
||||||
throw new Error(
|
|
||||||
`No registered room binding for IPC outbound JID: ${args.jid}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = resolveChannelForDeliveryRole(
|
|
||||||
deps.channels,
|
|
||||||
args.jid,
|
|
||||||
deliveryRole,
|
|
||||||
);
|
|
||||||
if (!route.channel) throw new Error(`No channel for JID: ${args.jid}`);
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
{
|
{
|
||||||
transition: 'ipc:route',
|
transition: 'ipc:route',
|
||||||
chatJid: args.jid,
|
chatJid: args.jid,
|
||||||
runId: args.runId ?? null,
|
runId: args.runId ?? null,
|
||||||
senderRole: deliveryRole ?? null,
|
senderRole: deliveryRole ?? null,
|
||||||
requestedRoleChannel: route.requestedRoleChannelName,
|
|
||||||
selectedChannel: route.selectedChannelName,
|
|
||||||
usedRoleChannel: route.usedRoleChannel,
|
|
||||||
fallbackUsed: route.fallbackUsed,
|
|
||||||
},
|
},
|
||||||
'IPC relay routed message to canonical work item delivery',
|
'IPC relay routed message to canonical work item delivery',
|
||||||
);
|
);
|
||||||
|
|
||||||
const createWorkItem = deps.createWorkItem ?? createProducedWorkItem;
|
const result = await deliverCanonicalOutboundMessage(
|
||||||
const workItem = createWorkItem({
|
{
|
||||||
group_folder: group.folder,
|
jid: args.jid,
|
||||||
chat_jid: args.jid,
|
text: args.text,
|
||||||
agent_type: group.agentType ?? 'claude-code',
|
deliveryRole,
|
||||||
delivery_role: deliveryRole ?? null,
|
attachments: args.attachments,
|
||||||
start_seq: null,
|
},
|
||||||
end_seq: null,
|
{
|
||||||
result_payload: args.text,
|
channels: deps.channels,
|
||||||
attachments: args.attachments,
|
roomBindings: deps.roomBindings,
|
||||||
});
|
log,
|
||||||
|
createWorkItem: deps.createWorkItem,
|
||||||
|
deliverWorkItem: deps.deliverWorkItem,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const deliverWorkItem = deps.deliverWorkItem ?? deliverOpenWorkItem;
|
if (result !== 'delivered') {
|
||||||
const delivered = await deliverWorkItem({
|
return result;
|
||||||
channel: route.channel,
|
|
||||||
item: workItem as WorkItem,
|
|
||||||
log,
|
|
||||||
attachmentBaseDirs: group.workDir ? [group.workDir] : undefined,
|
|
||||||
isDuplicateOfLastBotFinal: () => false,
|
|
||||||
openContinuation: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!delivered) {
|
|
||||||
return 'queued_retry';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
deliverOpenWorkItem,
|
deliverOpenWorkItem,
|
||||||
processOpenWorkItemDelivery,
|
processOpenWorkItemDelivery,
|
||||||
} from './message-runtime-delivery.js';
|
} from './message-runtime-delivery.js';
|
||||||
|
import { deliverCanonicalOutboundMessage } from './ipc-outbound-delivery.js';
|
||||||
import { handleQueuedRunGates } from './message-runtime-gating.js';
|
import { handleQueuedRunGates } from './message-runtime-gating.js';
|
||||||
import {
|
import {
|
||||||
enqueuePendingHandoffs as enqueueClaimedServiceHandoffs,
|
enqueuePendingHandoffs as enqueueClaimedServiceHandoffs,
|
||||||
@@ -93,6 +94,21 @@ export interface MessageRuntimeDeps {
|
|||||||
clearSession: (groupFolder: string, opts?: { allRoles?: boolean }) => void;
|
clearSession: (groupFolder: string, opts?: { allRoles?: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deliverSessionCommandMessage(
|
||||||
|
deps: Pick<MessageRuntimeDeps, 'channels' | 'getRoomBindings'>,
|
||||||
|
chatJid: string,
|
||||||
|
text: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await deliverCanonicalOutboundMessage(
|
||||||
|
{ jid: chatJid, text },
|
||||||
|
{
|
||||||
|
channels: deps.channels,
|
||||||
|
roomBindings: deps.getRoomBindings,
|
||||||
|
log: logger,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||||
processGroupMessages: (
|
processGroupMessages: (
|
||||||
chatJid: string,
|
chatJid: string,
|
||||||
@@ -110,7 +126,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
chatJid: string,
|
chatJid: string,
|
||||||
messages: NewMessage[],
|
messages: NewMessage[],
|
||||||
): NewMessage[] => labelPairedSenders(deps.channels, chatJid, messages);
|
): NewMessage[] => labelPairedSenders(deps.channels, chatJid, messages);
|
||||||
|
|
||||||
const enqueueScopedGroupMessageCheck = buildScopedMessageCheckEnqueuer(
|
const enqueueScopedGroupMessageCheck = buildScopedMessageCheckEnqueuer(
|
||||||
deps.queue,
|
deps.queue,
|
||||||
);
|
);
|
||||||
@@ -587,7 +602,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
timezone: deps.timezone,
|
timezone: deps.timezone,
|
||||||
hasImplicitContinuationWindow: continuationTracker.has,
|
hasImplicitContinuationWindow: continuationTracker.has,
|
||||||
sessionCommandDeps: {
|
sessionCommandDeps: {
|
||||||
sendMessage: (text) => channel.sendMessage(chatJid, text),
|
sendMessage: (text) =>
|
||||||
|
deliverSessionCommandMessage(deps, chatJid, text),
|
||||||
setTyping: (typing) =>
|
setTyping: (typing) =>
|
||||||
channel.setTyping?.(chatJid, typing) ?? Promise.resolve(),
|
channel.setTyping?.(chatJid, typing) ?? Promise.resolve(),
|
||||||
runAgent: (prompt, onOutput) =>
|
runAgent: (prompt, onOutput) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user