diff --git a/src/container-runner.test.ts b/src/agent-runner.test.ts similarity index 91% rename from src/container-runner.test.ts rename to src/agent-runner.test.ts index 0b993cd..1c7e59e 100644 --- a/src/container-runner.test.ts +++ b/src/agent-runner.test.ts @@ -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, - 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, () => {}, diff --git a/src/container-runner.ts b/src/agent-runner.ts similarity index 94% rename from src/container-runner.ts rename to src/agent-runner.ts index 115cf58..20220a8 100644 --- a/src/container-runner.ts +++ b/src/agent-runner.ts @@ -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, -): Promise { + input: AgentInput, + onProcess: (proc: ChildProcess, processName: string) => void, + onOutput?: (output: AgentOutput) => Promise, +): Promise { 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', diff --git a/src/config.ts b/src/config.ts index f2b73e5..a086e10 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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 { diff --git a/src/db.ts b/src/db.ts index 56304c3..4973c8d 100644 --- a/src/db.ts +++ b/src/db.ts @@ -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 { 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: diff --git a/src/group-queue.test.ts b/src/group-queue.test.ts index ca2702a..f7ff600 100644 --- a/src/group-queue.test.ts +++ b/src/group-queue.test.ts @@ -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'); diff --git a/src/group-queue.ts b/src/group-queue.ts index e347b7b..141edb7 100644 --- a/src/group-queue.ts +++ b/src/group-queue.ts @@ -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 { 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)', ); } } diff --git a/src/index.ts b/src/index.ts index e499273..c6b9116 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { 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, + onOutput?: (output: AgentOutput) => Promise, ): 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 { ); 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 { 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 { 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 { 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) { diff --git a/src/ipc.ts b/src/ipc.ts index 7a972c0..eaa1b1d 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -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 { diff --git a/src/session-commands.ts b/src/session-commands.ts index 2fcc8f2..1a3b2fb 100644 --- a/src/session-commands.ts +++ b/src/session-commands.ts @@ -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; diff --git a/src/task-scheduler.ts b/src/task-scheduler.ts index d0abd2e..a0a9dee 100644 --- a/src/task-scheduler.ts +++ b/src/task-scheduler.ts @@ -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; @@ -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) diff --git a/src/token-refresh.ts b/src/token-refresh.ts index 27a3688..7e8f7c4 100644 --- a/src/token-refresh.ts +++ b/src/token-refresh.ts @@ -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', + ); } } diff --git a/src/types.ts b/src/types.ts index ebabf32..ff95140 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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;