refactor: rename container references to agent across codebase

- container-runner.ts → agent-runner.ts
- ContainerInput/Output → AgentInput/Output
- ContainerConfig → AgentConfig
- runContainerAgent → runAgentProcess
- CONTAINER_TIMEOUT → AGENT_TIMEOUT (with env fallback)
- MAX_CONCURRENT_CONTAINERS → MAX_CONCURRENT_AGENTS
- containerName → processName, isTaskContainer → isTaskProcess
- DB column container_config kept as-is (backwards compat)
This commit is contained in:
Eyejoker
2026-03-13 16:18:36 +09:00
parent 35277cea0a
commit bd7f4408ae
12 changed files with 144 additions and 133 deletions

View File

@@ -2,14 +2,14 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { EventEmitter } from 'events';
import { PassThrough } from 'stream';
// Sentinel markers must match container-runner.ts
// Sentinel markers must match agent-runner.ts
const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
// Mock config
vi.mock('./config.js', () => ({
CONTAINER_MAX_OUTPUT_SIZE: 10485760,
CONTAINER_TIMEOUT: 1800000, // 30min
AGENT_MAX_OUTPUT_SIZE: 10485760,
AGENT_TIMEOUT: 1800000, // 30min
DATA_DIR: '/tmp/nanoclaw-test-data',
GROUPS_DIR: '/tmp/nanoclaw-test-groups',
IDLE_TIMEOUT: 1800000, // 30min
@@ -89,7 +89,7 @@ vi.mock('child_process', async () => {
};
});
import { runContainerAgent, ContainerOutput } from './container-runner.js';
import { runAgentProcess, AgentOutput } from './agent-runner.js';
import type { RegisteredGroup } from './types.js';
const testGroup: RegisteredGroup = {
@@ -108,13 +108,13 @@ const testInput = {
function emitOutputMarker(
proc: ReturnType<typeof createFakeProcess>,
output: ContainerOutput,
output: AgentOutput,
) {
const json = JSON.stringify(output);
proc.stdout.push(`${OUTPUT_START_MARKER}\n${json}\n${OUTPUT_END_MARKER}\n`);
}
describe('container-runner timeout behavior', () => {
describe('agent-runner timeout behavior', () => {
beforeEach(() => {
vi.useFakeTimers();
fakeProc = createFakeProcess();
@@ -126,7 +126,7 @@ describe('container-runner timeout behavior', () => {
it('timeout after output resolves as success', async () => {
const onOutput = vi.fn(async () => {});
const resultPromise = runContainerAgent(
const resultPromise = runAgentProcess(
testGroup,
testInput,
() => {},
@@ -146,7 +146,7 @@ describe('container-runner timeout behavior', () => {
// Fire the hard timeout (IDLE_TIMEOUT + 30s = 1830000ms)
await vi.advanceTimersByTimeAsync(1830000);
// Emit close event (as if container was stopped by the timeout)
// Emit close event (as if agent was stopped by the timeout)
fakeProc.emit('close', 137);
// Let the promise resolve
@@ -162,7 +162,7 @@ describe('container-runner timeout behavior', () => {
it('timeout with no output resolves as error', async () => {
const onOutput = vi.fn(async () => {});
const resultPromise = runContainerAgent(
const resultPromise = runAgentProcess(
testGroup,
testInput,
() => {},
@@ -185,7 +185,7 @@ describe('container-runner timeout behavior', () => {
it('normal exit after output resolves as success', async () => {
const onOutput = vi.fn(async () => {});
const resultPromise = runContainerAgent(
const resultPromise = runAgentProcess(
testGroup,
testInput,
() => {},

View File

@@ -8,8 +8,8 @@ import os from 'os';
import path from 'path';
import {
CONTAINER_MAX_OUTPUT_SIZE,
CONTAINER_TIMEOUT,
AGENT_MAX_OUTPUT_SIZE,
AGENT_TIMEOUT,
DATA_DIR,
GROUPS_DIR,
IDLE_TIMEOUT,
@@ -24,7 +24,7 @@ import { RegisteredGroup } from './types.js';
const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
export interface ContainerInput {
export interface AgentInput {
prompt: string;
sessionId?: string;
groupFolder: string;
@@ -35,7 +35,7 @@ export interface ContainerInput {
agentType?: 'claude-code' | 'codex';
}
export interface ContainerOutput {
export interface AgentOutput {
status: 'success' | 'error';
result: string | null;
newSessionId?: string;
@@ -137,8 +137,8 @@ function prepareGroupEnvironment(
// Additional mount directories (validated)
const extraDirs: string[] = [];
if (group.containerConfig?.additionalMounts) {
for (const mount of group.containerConfig.additionalMounts) {
if (group.agentConfig?.additionalMounts) {
for (const mount of group.agentConfig.additionalMounts) {
if (fs.existsSync(mount.hostPath)) {
extraDirs.push(mount.hostPath);
}
@@ -212,12 +212,12 @@ function prepareGroupEnvironment(
// Codex model/effort configuration (per-group overrides global)
const codexModel =
group.containerConfig?.codexModel ||
group.agentConfig?.codexModel ||
envVars.CODEX_MODEL ||
process.env.CODEX_MODEL;
if (codexModel) env.CODEX_MODEL = codexModel;
const codexEffort =
group.containerConfig?.codexEffort ||
group.agentConfig?.codexEffort ||
envVars.CODEX_EFFORT ||
process.env.CODEX_EFFORT;
if (codexEffort) env.CODEX_EFFORT = codexEffort;
@@ -321,23 +321,23 @@ NANOCLAW_IS_MAIN = ${JSON.stringify(isMain ? '1' : '0')}
const val = envVars[key as keyof typeof envVars] || process.env[key];
if (val) env[key] = val;
}
if (group.containerConfig?.claudeModel) {
env.CLAUDE_MODEL = group.containerConfig.claudeModel;
if (group.agentConfig?.claudeModel) {
env.CLAUDE_MODEL = group.agentConfig.claudeModel;
}
if (group.containerConfig?.claudeEffort) {
env.CLAUDE_EFFORT = group.containerConfig.claudeEffort;
if (group.agentConfig?.claudeEffort) {
env.CLAUDE_EFFORT = group.agentConfig.claudeEffort;
}
}
return { env, groupDir, runnerDir };
}
export async function runContainerAgent(
export async function runAgentProcess(
group: RegisteredGroup,
input: ContainerInput,
onProcess: (proc: ChildProcess, containerName: string) => void,
onOutput?: (output: ContainerOutput) => Promise<void>,
): Promise<ContainerOutput> {
input: AgentInput,
onProcess: (proc: ChildProcess, processName: string) => void,
onOutput?: (output: AgentOutput) => Promise<void>,
): Promise<AgentOutput> {
const startTime = Date.now();
const { env, groupDir, runnerDir } = prepareGroupEnvironment(
group,
@@ -400,7 +400,7 @@ export async function runContainerAgent(
const chunk = data.toString();
if (!stdoutTruncated) {
const remaining = CONTAINER_MAX_OUTPUT_SIZE - stdout.length;
const remaining = AGENT_MAX_OUTPUT_SIZE - stdout.length;
if (chunk.length > remaining) {
stdout += chunk.slice(0, remaining);
stdoutTruncated = true;
@@ -426,7 +426,7 @@ export async function runContainerAgent(
parseBuffer = parseBuffer.slice(endIdx + OUTPUT_END_MARKER.length);
try {
const parsed: ContainerOutput = JSON.parse(jsonStr);
const parsed: AgentOutput = JSON.parse(jsonStr);
if (parsed.newSessionId) {
newSessionId = parsed.newSessionId;
}
@@ -450,7 +450,7 @@ export async function runContainerAgent(
if (line) logger.debug({ agent: group.folder }, line);
}
if (stderrTruncated) return;
const remaining = CONTAINER_MAX_OUTPUT_SIZE - stderr.length;
const remaining = AGENT_MAX_OUTPUT_SIZE - stderr.length;
if (chunk.length > remaining) {
stderr += chunk.slice(0, remaining);
stderrTruncated = true;
@@ -461,7 +461,7 @@ export async function runContainerAgent(
let timedOut = false;
let hadStreamingOutput = false;
const configTimeout = group.containerConfig?.timeout || CONTAINER_TIMEOUT;
const configTimeout = group.agentConfig?.timeout || AGENT_TIMEOUT;
const timeoutMs = Math.max(configTimeout, IDLE_TIMEOUT + 30_000);
const killOnTimeout = () => {
@@ -596,7 +596,7 @@ export async function runContainerAgent(
const lines = stdout.trim().split('\n');
jsonLine = lines[lines.length - 1];
}
const output: ContainerOutput = JSON.parse(jsonLine);
const output: AgentOutput = JSON.parse(jsonLine);
logger.info(
{ group: group.name, duration, status: output.status },
'Agent completed',

View File

@@ -32,19 +32,19 @@ export const DATA_DIR = path.resolve(
process.env.NANOCLAW_DATA_DIR || path.join(PROJECT_ROOT, 'data'),
);
export const CONTAINER_TIMEOUT = parseInt(
process.env.CONTAINER_TIMEOUT || '1800000',
export const AGENT_TIMEOUT = parseInt(
process.env.AGENT_TIMEOUT || process.env.CONTAINER_TIMEOUT || '1800000',
10,
);
export const CONTAINER_MAX_OUTPUT_SIZE = parseInt(
process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760',
export const AGENT_MAX_OUTPUT_SIZE = parseInt(
process.env.AGENT_MAX_OUTPUT_SIZE || process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760',
10,
); // 10MB default
export const IPC_POLL_INTERVAL = 1000;
export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30min default — how long to keep container alive after last result
export const MAX_CONCURRENT_CONTAINERS = Math.max(
export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30min default — how long to keep agent alive after last result
export const MAX_CONCURRENT_AGENTS = Math.max(
1,
parseInt(process.env.MAX_CONCURRENT_CONTAINERS || '5', 10) || 5,
parseInt(process.env.MAX_CONCURRENT_AGENTS || process.env.MAX_CONCURRENT_CONTAINERS || '5', 10) || 5,
);
function escapeRegex(str: string): string {

View File

@@ -600,7 +600,7 @@ export function getRegisteredGroup(
folder: row.folder,
trigger: row.trigger_pattern,
added_at: row.added_at,
containerConfig: row.container_config
agentConfig: row.container_config
? JSON.parse(row.container_config)
: undefined,
requiresTrigger:
@@ -624,7 +624,7 @@ export function setRegisteredGroup(jid: string, group: RegisteredGroup): void {
group.folder,
group.trigger,
group.added_at,
group.containerConfig ? JSON.stringify(group.containerConfig) : null,
group.agentConfig ? JSON.stringify(group.agentConfig) : null,
group.requiresTrigger === undefined ? 1 : group.requiresTrigger ? 1 : 0,
group.isMain ? 1 : 0,
group.agentType || 'claude-code',
@@ -659,7 +659,7 @@ export function getAllRegisteredGroups(): Record<string, RegisteredGroup> {
folder: row.folder,
trigger: row.trigger_pattern,
added_at: row.added_at,
containerConfig: row.container_config
agentConfig: row.container_config
? JSON.parse(row.container_config)
: undefined,
requiresTrigger:

View File

@@ -5,7 +5,7 @@ import { GroupQueue } from './group-queue.js';
// Mock config to control concurrency limit
vi.mock('./config.js', () => ({
DATA_DIR: '/tmp/nanoclaw-test-data',
MAX_CONCURRENT_CONTAINERS: 2,
MAX_CONCURRENT_AGENTS: 2,
}));
// Mock fs operations used by sendMessage/closeStdin
@@ -36,7 +36,7 @@ describe('GroupQueue', () => {
// --- Single group at a time ---
it('only runs one container per group at a time', async () => {
it('only runs one agent per group at a time', async () => {
let concurrentCount = 0;
let maxConcurrent = 0;
@@ -87,7 +87,7 @@ describe('GroupQueue', () => {
// Let promises settle
await vi.advanceTimersByTimeAsync(10);
// Only 2 should be active (MAX_CONCURRENT_CONTAINERS = 2)
// Only 2 should be active (MAX_CONCURRENT_AGENTS = 2)
expect(maxActive).toBe(2);
expect(activeCount).toBe(2);
@@ -280,7 +280,7 @@ describe('GroupQueue', () => {
// --- Idle preemption ---
it('does NOT preempt active container when not idle', async () => {
it('does NOT preempt active agent when not idle', async () => {
const fs = await import('fs');
let resolveProcess: () => void;
@@ -301,15 +301,15 @@ describe('GroupQueue', () => {
queue.registerProcess(
'group1@g.us',
{} as any,
'container-1',
'agent-1',
'test-group',
);
// Enqueue a task while container is active but NOT idle
// Enqueue a task while agent is active but NOT idle
const taskFn = vi.fn(async () => {});
queue.enqueueTask('group1@g.us', 'task-1', taskFn);
// _close should NOT have been written (container is working, not idle)
// _close should NOT have been written (agent is working, not idle)
const writeFileSync = vi.mocked(fs.default.writeFileSync);
const closeWrites = writeFileSync.mock.calls.filter(
(call) => typeof call[0] === 'string' && call[0].endsWith('_close'),
@@ -320,7 +320,7 @@ describe('GroupQueue', () => {
await vi.advanceTimersByTimeAsync(10);
});
it('preempts idle container when task is enqueued', async () => {
it('preempts idle agent when task is enqueued', async () => {
const fs = await import('fs');
let resolveProcess: () => void;
@@ -341,7 +341,7 @@ describe('GroupQueue', () => {
queue.registerProcess(
'group1@g.us',
{} as any,
'container-1',
'agent-1',
'test-group',
);
queue.notifyIdle('group1@g.us');
@@ -353,7 +353,7 @@ describe('GroupQueue', () => {
const taskFn = vi.fn(async () => {});
queue.enqueueTask('group1@g.us', 'task-1', taskFn);
// _close SHOULD have been written (container is idle)
// _close SHOULD have been written (agent is idle)
const closeWrites = writeFileSync.mock.calls.filter(
(call) => typeof call[0] === 'string' && call[0].endsWith('_close'),
);
@@ -380,11 +380,11 @@ describe('GroupQueue', () => {
queue.registerProcess(
'group1@g.us',
{} as any,
'container-1',
'agent-1',
'test-group',
);
// Container becomes idle
// Agent becomes idle
queue.notifyIdle('group1@g.us');
// A new user message arrives — resets idleWaiting
@@ -406,7 +406,7 @@ describe('GroupQueue', () => {
await vi.advanceTimersByTimeAsync(10);
});
it('sendMessage returns false for task containers so user messages queue up', async () => {
it('sendMessage returns false for task agents so user messages queue up', async () => {
let resolveTask: () => void;
const taskFn = vi.fn(async () => {
@@ -415,17 +415,17 @@ describe('GroupQueue', () => {
});
});
// Start a task (sets isTaskContainer = true)
// Start a task (sets isTaskProcess = true)
queue.enqueueTask('group1@g.us', 'task-1', taskFn);
await vi.advanceTimersByTimeAsync(10);
queue.registerProcess(
'group1@g.us',
{} as any,
'container-1',
'agent-1',
'test-group',
);
// sendMessage should return false — user messages must not go to task containers
// sendMessage should return false — user messages must not go to task agents
const result = queue.sendMessage('group1@g.us', 'hello');
expect(result).toBe(false);
@@ -454,7 +454,7 @@ describe('GroupQueue', () => {
queue.registerProcess(
'group1@g.us',
{} as any,
'container-1',
'agent-1',
'test-group',
);
@@ -469,7 +469,7 @@ describe('GroupQueue', () => {
);
expect(closeWrites).toHaveLength(0);
// Now container becomes idle — should preempt because task is pending
// Now agent becomes idle — should preempt because task is pending
writeFileSync.mockClear();
queue.notifyIdle('group1@g.us');

View File

@@ -2,7 +2,7 @@ import { ChildProcess } from 'child_process';
import fs from 'fs';
import path from 'path';
import { DATA_DIR, MAX_CONCURRENT_CONTAINERS } from './config.js';
import { DATA_DIR, MAX_CONCURRENT_AGENTS } from './config.js';
import { logger } from './logger.js';
interface QueuedTask {
@@ -17,12 +17,12 @@ const BASE_RETRY_MS = 5000;
interface GroupState {
active: boolean;
idleWaiting: boolean;
isTaskContainer: boolean;
isTaskProcess: boolean;
runningTaskId: string | null;
pendingMessages: boolean;
pendingTasks: QueuedTask[];
process: ChildProcess | null;
containerName: string | null;
processName: string | null;
groupFolder: string | null;
retryCount: number;
}
@@ -41,12 +41,12 @@ export class GroupQueue {
state = {
active: false,
idleWaiting: false,
isTaskContainer: false,
isTaskProcess: false,
runningTaskId: null,
pendingMessages: false,
pendingTasks: [],
process: null,
containerName: null,
processName: null,
groupFolder: null,
retryCount: 0,
};
@@ -64,18 +64,18 @@ export class GroupQueue {
const state = this.getGroup(groupJid);
// Pre-set groupFolder so sendMessage can pipe IPC while container starts
// Pre-set groupFolder so sendMessage can pipe IPC while agent process starts
if (groupFolder && !state.groupFolder) {
state.groupFolder = groupFolder;
}
if (state.active) {
state.pendingMessages = true;
logger.debug({ groupJid }, 'Container active, message queued');
logger.debug({ groupJid }, 'Agent active, message queued');
return;
}
if (this.activeCount >= MAX_CONCURRENT_CONTAINERS) {
if (this.activeCount >= MAX_CONCURRENT_AGENTS) {
state.pendingMessages = true;
if (!this.waitingGroups.includes(groupJid)) {
this.waitingGroups.push(groupJid);
@@ -112,11 +112,11 @@ export class GroupQueue {
if (state.idleWaiting) {
this.closeStdin(groupJid);
}
logger.debug({ groupJid, taskId }, 'Container active, task queued');
logger.debug({ groupJid, taskId }, 'Agent active, task queued');
return;
}
if (this.activeCount >= MAX_CONCURRENT_CONTAINERS) {
if (this.activeCount >= MAX_CONCURRENT_AGENTS) {
state.pendingTasks.push({ id: taskId, groupJid, fn });
if (!this.waitingGroups.includes(groupJid)) {
this.waitingGroups.push(groupJid);
@@ -137,18 +137,18 @@ export class GroupQueue {
registerProcess(
groupJid: string,
proc: ChildProcess,
containerName: string,
processName: string,
groupFolder?: string,
): void {
const state = this.getGroup(groupJid);
state.process = proc;
state.containerName = containerName;
state.processName = processName;
if (groupFolder) state.groupFolder = groupFolder;
}
/**
* Mark the container as idle-waiting (finished work, waiting for IPC input).
* If tasks are pending, preempt the idle container immediately.
* Mark the agent process as idle-waiting (finished work, waiting for IPC input).
* If tasks are pending, preempt the idle agent process immediately.
*/
notifyIdle(groupJid: string): void {
const state = this.getGroup(groupJid);
@@ -159,12 +159,12 @@ export class GroupQueue {
}
/**
* Send a follow-up message to the active container via IPC file.
* Returns true if the message was written, false if no active container.
* Send a follow-up message to the active agent process via IPC file.
* Returns true if the message was written, false if no active agent process.
*/
sendMessage(groupJid: string, text: string): boolean {
const state = this.getGroup(groupJid);
if (!state.active || !state.groupFolder || state.isTaskContainer)
if (!state.active || !state.groupFolder || state.isTaskProcess)
return false;
state.idleWaiting = false; // Agent is about to receive work, no longer idle
@@ -183,7 +183,7 @@ export class GroupQueue {
}
/**
* Signal the active container to wind down by writing a close sentinel.
* Signal the active agent process to wind down by writing a close sentinel.
*/
closeStdin(groupJid: string): void {
const state = this.getGroup(groupJid);
@@ -205,13 +205,13 @@ export class GroupQueue {
const state = this.getGroup(groupJid);
state.active = true;
state.idleWaiting = false;
state.isTaskContainer = false;
state.isTaskProcess = false;
state.pendingMessages = false;
this.activeCount++;
logger.debug(
{ groupJid, reason, activeCount: this.activeCount },
'Starting container for group',
'Starting agent process for group',
);
try {
@@ -229,7 +229,7 @@ export class GroupQueue {
} finally {
state.active = false;
state.process = null;
state.containerName = null;
state.processName = null;
state.groupFolder = null;
this.activeCount--;
this.drainGroup(groupJid);
@@ -240,7 +240,7 @@ export class GroupQueue {
const state = this.getGroup(groupJid);
state.active = true;
state.idleWaiting = false;
state.isTaskContainer = true;
state.isTaskProcess = true;
state.runningTaskId = task.id;
this.activeCount++;
@@ -255,10 +255,10 @@ export class GroupQueue {
logger.error({ groupJid, taskId: task.id, err }, 'Error running task');
} finally {
state.active = false;
state.isTaskContainer = false;
state.isTaskProcess = false;
state.runningTaskId = null;
state.process = null;
state.containerName = null;
state.processName = null;
state.groupFolder = null;
this.activeCount--;
this.drainGroup(groupJid);
@@ -323,7 +323,7 @@ export class GroupQueue {
private drainWaiting(): void {
while (
this.waitingGroups.length > 0 &&
this.activeCount < MAX_CONCURRENT_CONTAINERS
this.activeCount < MAX_CONCURRENT_AGENTS
) {
const nextJid = this.waitingGroups.shift()!;
const state = this.getGroup(nextJid);
@@ -352,19 +352,19 @@ export class GroupQueue {
async shutdown(_gracePeriodMs: number): Promise<void> {
this.shuttingDown = true;
// Count active containers but don't kill them — they'll finish on their own
// via idle timeout or container timeout. The --rm flag cleans them up on exit.
// This prevents WhatsApp reconnection restarts from killing working agents.
const activeContainers: string[] = [];
// Count active agent processes but don't kill them — they'll finish on their own
// via idle timeout or agent timeout.
// This prevents reconnection restarts from killing working agents.
const activeProcesses: string[] = [];
for (const [jid, state] of this.groups) {
if (state.process && !state.process.killed && state.containerName) {
activeContainers.push(state.containerName);
if (state.process && !state.process.killed && state.processName) {
activeProcesses.push(state.processName);
}
}
logger.info(
{ activeCount: this.activeCount, detachedContainers: activeContainers },
'GroupQueue shutting down (containers detached, not killed)',
{ activeCount: this.activeCount, detachedProcesses: activeProcesses },
'GroupQueue shutting down (agent processes detached, not killed)',
);
}
}

View File

@@ -14,11 +14,11 @@ import {
getRegisteredChannelNames,
} from './channels/registry.js';
import {
ContainerOutput,
runContainerAgent,
AgentOutput,
runAgentProcess,
writeGroupsSnapshot,
writeTasksSnapshot,
} from './container-runner.js';
} from './agent-runner.js';
import {
getAllChats,
getAllRegisteredGroups,
@@ -121,7 +121,7 @@ function registerGroup(jid: string, group: RegisteredGroup): void {
* Get available groups list for the agent.
* Returns groups ordered by most recent activity.
*/
export function getAvailableGroups(): import('./container-runner.js').AvailableGroup[] {
export function getAvailableGroups(): import('./agent-runner.js').AvailableGroup[] {
const chats = getAllChats();
const registeredJids = new Set(Object.keys(registeredGroups));
@@ -237,7 +237,7 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
idleTimer = setTimeout(() => {
logger.debug(
{ group: group.name },
'Idle timeout, closing container stdin',
'Idle timeout, closing agent stdin',
);
queue.closeStdin(chatJid);
}, IDLE_TIMEOUT);
@@ -307,12 +307,12 @@ async function runAgent(
group: RegisteredGroup,
prompt: string,
chatJid: string,
onOutput?: (output: ContainerOutput) => Promise<void>,
onOutput?: (output: AgentOutput) => Promise<void>,
): Promise<'success' | 'error'> {
const isMain = group.isMain === true;
const sessionId = sessions[group.folder];
// Update tasks snapshot for container to read (filtered by group)
// Update tasks snapshot for agent to read (filtered by group)
const tasks = getAllTasks();
writeTasksSnapshot(
group.folder,
@@ -339,7 +339,7 @@ async function runAgent(
// Wrap onOutput to track session ID from streamed results
const wrappedOnOutput = onOutput
? async (output: ContainerOutput) => {
? async (output: AgentOutput) => {
if (output.newSessionId) {
sessions[group.folder] = output.newSessionId;
setSession(group.folder, output.newSessionId);
@@ -349,7 +349,7 @@ async function runAgent(
: undefined;
try {
const output = await runContainerAgent(
const output = await runAgentProcess(
group,
{
prompt,
@@ -359,8 +359,8 @@ async function runAgent(
isMain,
assistantName: ASSISTANT_NAME,
},
(proc, containerName) =>
queue.registerProcess(chatJid, proc, containerName, group.folder),
(proc, processName) =>
queue.registerProcess(chatJid, proc, processName, group.folder),
wrappedOnOutput,
);
@@ -372,7 +372,7 @@ async function runAgent(
if (output.status === 'error') {
logger.error(
{ group: group.name, error: output.error },
'Container agent error',
'Agent process error',
);
return 'error';
}
@@ -461,9 +461,9 @@ async function startMessageLoop(): Promise<void> {
);
if (loopCmdMsg) {
// Only close active container if the sender is authorized — otherwise an
// Only close active agent if the sender is authorized — otherwise an
// untrusted user could kill in-flight work by sending /compact (DoS).
// closeStdin no-ops internally when no container is active.
// closeStdin no-ops internally when no agent is active.
if (
isSessionCommandAllowed(
isMainGroup,
@@ -473,7 +473,7 @@ async function startMessageLoop(): Promise<void> {
queue.closeStdin(chatJid);
}
// Enqueue so processGroupMessages handles auth + cursor advancement.
// Don't pipe via IPC — slash commands need a fresh container with
// Don't pipe via IPC — slash commands need a fresh agent with
// string prompt (not MessageStream) for SDK recognition.
queue.enqueueMessageCheck(chatJid);
continue;
@@ -510,19 +510,19 @@ async function startMessageLoop(): Promise<void> {
if (queue.sendMessage(chatJid, formatted)) {
logger.debug(
{ chatJid, count: messagesToSend.length },
'Piped messages to active container',
'Piped messages to active agent',
);
lastAgentTimestamp[chatJid] =
messagesToSend[messagesToSend.length - 1].timestamp;
saveState();
// Show typing indicator while the container processes the piped message
// Show typing indicator while the agent processes the piped message
channel
.setTyping?.(chatJid, true)
?.catch((err) =>
logger.warn({ chatJid, err }, 'Failed to set typing indicator'),
);
} else {
// No active container — enqueue for a new one
// No active agent — enqueue for a new one
queue.enqueueMessageCheck(chatJid, group.folder);
}
}
@@ -628,8 +628,8 @@ async function main(): Promise<void> {
registeredGroups: () => registeredGroups,
getSessions: () => sessions,
queue,
onProcess: (groupJid, proc, containerName, groupFolder) =>
queue.registerProcess(groupJid, proc, containerName, groupFolder),
onProcess: (groupJid, proc, processName, groupFolder) =>
queue.registerProcess(groupJid, proc, processName, groupFolder),
sendMessage: async (jid, rawText) => {
const channel = findChannel(channels, jid);
if (!channel) {

View File

@@ -4,7 +4,7 @@ import path from 'path';
import { CronExpressionParser } from 'cron-parser';
import { DATA_DIR, IPC_POLL_INTERVAL, TIMEZONE } from './config.js';
import { AvailableGroup } from './container-runner.js';
import { AvailableGroup } from './agent-runner.js';
import { createTask, deleteTask, getTaskById, updateTask } from './db.js';
import { isValidGroupFolder } from './group-folder.js';
import { logger } from './logger.js';
@@ -170,7 +170,7 @@ export async function processTaskIpc(
folder?: string;
trigger?: string;
requiresTrigger?: boolean;
containerConfig?: RegisteredGroup['containerConfig'];
agentConfig?: RegisteredGroup['agentConfig'];
},
sourceGroup: string, // Verified identity from IPC directory
isMain: boolean, // Verified from directory path
@@ -438,7 +438,7 @@ export async function processTaskIpc(
folder: data.folder,
trigger: data.trigger,
added_at: new Date().toISOString(),
containerConfig: data.containerConfig,
agentConfig: data.agentConfig,
requiresTrigger: data.requiresTrigger,
});
} else {

View File

@@ -26,7 +26,7 @@ export function isSessionCommandAllowed(
return isMainGroup || isFromMe;
}
/** Minimal agent result interface — matches the subset of ContainerOutput used here. */
/** Minimal agent result interface — matches the subset of AgentOutput used here. */
export interface AgentResult {
status: 'success' | 'error';
result?: string | object | null;

View File

@@ -4,10 +4,10 @@ import fs from 'fs';
import { ASSISTANT_NAME, SCHEDULER_POLL_INTERVAL, TIMEZONE } from './config.js';
import {
ContainerOutput,
runContainerAgent,
AgentOutput,
runAgentProcess,
writeTasksSnapshot,
} from './container-runner.js';
} from './agent-runner.js';
import {
getAllTasks,
getDueTasks,
@@ -69,7 +69,7 @@ export interface SchedulerDependencies {
onProcess: (
groupJid: string,
proc: ChildProcess,
containerName: string,
processName: string,
groupFolder: string,
) => void;
sendMessage: (jid: string, text: string) => Promise<void>;
@@ -129,7 +129,7 @@ async function runTask(
return;
}
// Update tasks snapshot for container to read (filtered by group)
// Update tasks snapshot for agent to read (filtered by group)
const isMain = group.isMain === true;
const tasks = getAllTasks();
writeTasksSnapshot(
@@ -154,7 +154,7 @@ async function runTask(
const sessionId =
task.context_mode === 'group' ? sessions[task.group_folder] : undefined;
// After the task produces a result, close the container promptly.
// After the task produces a result, close the agent promptly.
// Tasks are single-turn — no need to wait IDLE_TIMEOUT (30 min) for the
// query loop to time out. A short delay handles any final MCP calls.
const TASK_CLOSE_DELAY_MS = 10000;
@@ -163,13 +163,13 @@ async function runTask(
const scheduleClose = () => {
if (closeTimer) return; // already scheduled
closeTimer = setTimeout(() => {
logger.debug({ taskId: task.id }, 'Closing task container after result');
logger.debug({ taskId: task.id }, 'Closing task agent after result');
deps.queue.closeStdin(task.chat_jid);
}, TASK_CLOSE_DELAY_MS);
};
try {
const output = await runContainerAgent(
const output = await runAgentProcess(
group,
{
prompt: task.prompt,
@@ -180,9 +180,9 @@ async function runTask(
isScheduledTask: true,
assistantName: ASSISTANT_NAME,
},
(proc, containerName) =>
deps.onProcess(task.chat_jid, proc, containerName, task.group_folder),
async (streamedOutput: ContainerOutput) => {
(proc, processName) =>
deps.onProcess(task.chat_jid, proc, processName, task.group_folder),
async (streamedOutput: AgentOutput) => {
if (streamedOutput.result) {
result = streamedOutput.result;
// Forward result to user (sendMessage handles formatting)

View File

@@ -81,17 +81,28 @@ function syncToSessionDirs(credsPath: string): void {
const groups = fs.readdirSync(sessionsDir);
let synced = 0;
for (const group of groups) {
const dest = path.join(sessionsDir, group, '.claude', '.credentials.json');
const dest = path.join(
sessionsDir,
group,
'.claude',
'.credentials.json',
);
if (fs.existsSync(path.dirname(dest))) {
fs.copyFileSync(credsPath, dest);
synced++;
}
}
if (synced > 0) {
logger.info({ count: synced }, 'Synced credentials to session directories');
logger.info(
{ count: synced },
'Synced credentials to session directories',
);
}
} catch (err) {
logger.warn({ err: err instanceof Error ? err.message : String(err) }, 'Failed to sync credentials to sessions');
logger.warn(
{ err: err instanceof Error ? err.message : String(err) },
'Failed to sync credentials to sessions',
);
}
}

View File

@@ -4,7 +4,7 @@ export interface AdditionalMount {
readonly?: boolean; // Default: true for safety
}
export interface ContainerConfig {
export interface AgentConfig {
additionalMounts?: AdditionalMount[];
timeout?: number; // Default: 300000 (5 minutes)
// Per-group model/effort overrides (take precedence over global env vars)
@@ -21,7 +21,7 @@ export interface RegisteredGroup {
folder: string;
trigger: string;
added_at: string;
containerConfig?: ContainerConfig;
agentConfig?: AgentConfig;
requiresTrigger?: boolean; // Default: true for groups, false for solo chats
isMain?: boolean; // True for the main control group (no trigger, elevated privileges)
agentType?: AgentType;