feat: add 'both' agent type for shared Claude Code + Codex channels
- AgentType now supports 'both' — runs both agents sequentially - Each agent gets its own folder suffix (_cc / _codex) for isolated sessions - Claude Code bot handles message storage for 'both' channels - Responses routed through the matching bot for each agent type
This commit is contained in:
@@ -249,7 +249,11 @@ export class DiscordChannel implements Channel {
|
|||||||
if (!this.agentTypeFilter) return true;
|
if (!this.agentTypeFilter) return true;
|
||||||
const group = this.opts.registeredGroups()[jid];
|
const group = this.opts.registeredGroups()[jid];
|
||||||
if (!group) return false;
|
if (!group) return false;
|
||||||
return (group.agentType || 'claude-code') === this.agentTypeFilter;
|
const groupType = group.agentType || 'claude-code';
|
||||||
|
// 'both' channels are owned by the claude-code bot (primary handler for
|
||||||
|
// message storage and typing). The codex bot skips them to avoid duplicates.
|
||||||
|
if (groupType === 'both') return this.agentTypeFilter === 'claude-code';
|
||||||
|
return groupType === this.agentTypeFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect(): Promise<void> {
|
async disconnect(): Promise<void> {
|
||||||
|
|||||||
101
src/index.ts
101
src/index.ts
@@ -239,46 +239,86 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
|||||||
}, IDLE_TIMEOUT);
|
}, IDLE_TIMEOUT);
|
||||||
};
|
};
|
||||||
|
|
||||||
await channel.setTyping?.(chatJid, true);
|
// Determine which agent types to run
|
||||||
|
const agentTypes: Array<'claude-code' | 'codex'> =
|
||||||
|
group.agentType === 'both'
|
||||||
|
? ['claude-code', 'codex']
|
||||||
|
: [group.agentType || 'claude-code'];
|
||||||
|
|
||||||
let hadError = false;
|
let hadError = false;
|
||||||
let outputSentToUser = false;
|
let outputSentToUser = false;
|
||||||
|
|
||||||
const output = await runAgent(group, prompt, chatJid, async (result) => {
|
for (const agentType of agentTypes) {
|
||||||
// Streaming output callback — called for each agent result
|
// For 'both' groups, create a variant group with the specific agent type
|
||||||
if (result.result) {
|
// and a separate folder so each agent has its own session/workspace
|
||||||
const raw =
|
const effectiveGroup: RegisteredGroup =
|
||||||
typeof result.result === 'string'
|
group.agentType === 'both'
|
||||||
? result.result
|
? {
|
||||||
: JSON.stringify(result.result);
|
...group,
|
||||||
// Strip <internal>...</internal> blocks — agent uses these for internal reasoning
|
agentType,
|
||||||
const text = raw.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
|
folder: `${group.folder}_${agentType === 'claude-code' ? 'cc' : 'codex'}`,
|
||||||
logger.info({ group: group.name }, `Agent output: ${raw.slice(0, 200)}`);
|
}
|
||||||
if (text) {
|
: group;
|
||||||
await channel.sendMessage(chatJid, text);
|
|
||||||
outputSentToUser = true;
|
|
||||||
}
|
|
||||||
// Clear typing indicator after sending a response — the runner may stay
|
|
||||||
// alive waiting for IPC messages, but the user shouldn't see "typing..."
|
|
||||||
await channel.setTyping?.(chatJid, false);
|
|
||||||
// Only reset idle timer on actual results, not session-update markers (result: null)
|
|
||||||
resetIdleTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.status === 'success') {
|
// Find the right channel to send responses through
|
||||||
queue.notifyIdle(chatJid);
|
const sendChannel =
|
||||||
}
|
group.agentType === 'both'
|
||||||
|
? channels.find(
|
||||||
|
(c) =>
|
||||||
|
c.name.includes('discord') &&
|
||||||
|
c.name.includes(
|
||||||
|
agentType === 'codex' ? 'codex' : 'claude-code',
|
||||||
|
),
|
||||||
|
) || channel
|
||||||
|
: channel;
|
||||||
|
|
||||||
if (result.status === 'error') {
|
await sendChannel.setTyping?.(chatJid, true);
|
||||||
|
|
||||||
|
const output = await runAgent(
|
||||||
|
effectiveGroup,
|
||||||
|
prompt,
|
||||||
|
chatJid,
|
||||||
|
async (result) => {
|
||||||
|
if (result.result) {
|
||||||
|
const raw =
|
||||||
|
typeof result.result === 'string'
|
||||||
|
? result.result
|
||||||
|
: JSON.stringify(result.result);
|
||||||
|
const text = raw
|
||||||
|
.replace(/<internal>[\s\S]*?<\/internal>/g, '')
|
||||||
|
.trim();
|
||||||
|
logger.info(
|
||||||
|
{ group: group.name, agentType },
|
||||||
|
`Agent output: ${raw.slice(0, 200)}`,
|
||||||
|
);
|
||||||
|
if (text) {
|
||||||
|
await sendChannel.sendMessage(chatJid, text);
|
||||||
|
outputSentToUser = true;
|
||||||
|
}
|
||||||
|
await sendChannel.setTyping?.(chatJid, false);
|
||||||
|
resetIdleTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
queue.notifyIdle(chatJid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 'error') {
|
||||||
|
hadError = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendChannel.setTyping?.(chatJid, false);
|
||||||
|
|
||||||
|
if (output === 'error') {
|
||||||
hadError = true;
|
hadError = true;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
await channel.setTyping?.(chatJid, false);
|
|
||||||
if (idleTimer) clearTimeout(idleTimer);
|
if (idleTimer) clearTimeout(idleTimer);
|
||||||
|
|
||||||
if (output === 'error' || hadError) {
|
if (hadError) {
|
||||||
// If we already sent output to the user, don't roll back the cursor —
|
|
||||||
// the user got their response and re-processing would send duplicates.
|
|
||||||
if (outputSentToUser) {
|
if (outputSentToUser) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ group: group.name },
|
{ group: group.name },
|
||||||
@@ -286,7 +326,6 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Roll back cursor so retries can re-process these messages
|
|
||||||
lastAgentTimestamp[chatJid] = previousCursor;
|
lastAgentTimestamp[chatJid] = previousCursor;
|
||||||
saveState();
|
saveState();
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export interface ContainerConfig {
|
|||||||
timeout?: number; // Default: 300000 (5 minutes)
|
timeout?: number; // Default: 300000 (5 minutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentType = 'claude-code' | 'codex';
|
export type AgentType = 'claude-code' | 'codex' | 'both';
|
||||||
|
|
||||||
export interface RegisteredGroup {
|
export interface RegisteredGroup {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user