Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getContainerImageBase, getDefaultContainerImage, getInstallSlug } from
import { isValidTimezone } from './timezone.js';

// Read config values from .env (falls back to process.env).
const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER', 'ONECLI_URL', 'ONECLI_API_KEY', 'TZ']);
const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER', 'ONECLI_URL', 'ONECLI_API_KEY', 'TZ', 'NANOCLAW_CONTAINER_LOGS']);

export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy';
export const ASSISTANT_HAS_OWN_NUMBER =
Expand All @@ -33,6 +33,7 @@ export const INSTALL_SLUG = getInstallSlug(PROJECT_ROOT);
export const CONTAINER_INSTALL_LABEL = `nanoclaw-install=${INSTALL_SLUG}`;
export const CONTAINER_TIMEOUT = parseInt(process.env.CONTAINER_TIMEOUT || '1800000', 10);
export const CONTAINER_MAX_OUTPUT_SIZE = parseInt(process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760', 10); // 10MB default
export const CONTAINER_LOGS_ENABLED = (process.env.NANOCLAW_CONTAINER_LOGS || envConfig.NANOCLAW_CONTAINER_LOGS) === 'enabled';
export const ONECLI_URL = process.env.ONECLI_URL || envConfig.ONECLI_URL;
export const ONECLI_API_KEY = process.env.ONECLI_API_KEY || envConfig.ONECLI_API_KEY;
export const MAX_MESSAGES_PER_PROMPT = Math.max(1, parseInt(process.env.MAX_MESSAGES_PER_PROMPT || '10', 10) || 10);
Expand Down
36 changes: 25 additions & 11 deletions src/container-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CONTAINER_IMAGE,
CONTAINER_IMAGE_BASE,
CONTAINER_INSTALL_LABEL,
CONTAINER_LOGS_ENABLED,
DATA_DIR,
GROUPS_DIR,
ONECLI_API_KEY,
Expand Down Expand Up @@ -155,21 +156,34 @@ async function spawnContainer(session: Session): Promise<void> {
// immediate kill before the new container touches the file itself.
fs.rmSync(heartbeatPath(agentGroup.id, session.id), { force: true });

const container = spawn(CONTAINER_RUNTIME_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'] });
// Persist container stdout+stderr to a per-instance file via inherited fd.
// Kernel-level write — the host process is uninvolved once spawn() returns,
// so a slow disk or full filesystem hits the container's writes, not us.
// Files accumulate in logs/containers/<group>/<containerName>.log; prune
// manually (rm) or via cron. Off by default — opt in with
// NANOCLAW_CONTAINER_LOGS=enabled in .env or the environment.
let logFd: number | undefined;
if (CONTAINER_LOGS_ENABLED) {
try {
const logDir = path.join(process.cwd(), 'logs', 'containers', agentGroup.folder);
fs.mkdirSync(logDir, { recursive: true });
logFd = fs.openSync(path.join(logDir, `${containerName}.log`), 'a');
} catch (err) {
log.warn('Container log file open failed — proceeding without persistence', {
containerName,
err,
});
}
}

const stdio: Array<'ignore' | number> =
logFd !== undefined ? ['ignore', logFd, logFd] : ['ignore', 'ignore', 'ignore'];
const container = spawn(CONTAINER_RUNTIME_BIN, args, { stdio });
if (logFd !== undefined) fs.closeSync(logFd);

activeContainers.set(session.id, { process: container, containerName });
markContainerRunning(session.id);

// Log stderr
container.stderr?.on('data', (data) => {
for (const line of data.toString().trim().split('\n')) {
if (line) log.debug(line, { container: agentGroup.folder });
}
});

// stdout is unused in v2 (all IO is via session DB)
container.stdout?.on('data', () => {});

// No host-side idle timeout. Stale/stuck detection is driven by the host
// sweep reading heartbeat mtime + processing_ack claim age + container_state
// (see src/host-sweep.ts). This avoids killing long-running legitimate work
Expand Down
Loading