diff --git a/container/agent-runner/src/db/connection.ts b/container/agent-runner/src/db/connection.ts index 3ca44a8..871e43a 100644 --- a/container/agent-runner/src/db/connection.ts +++ b/container/agent-runner/src/db/connection.ts @@ -27,21 +27,29 @@ const DEFAULT_HEARTBEAT_PATH = '/workspace/.heartbeat'; let _inbound: Database | null = null; let _outbound: Database | null = null; let _heartbeatPath: string = DEFAULT_HEARTBEAT_PATH; +let _testMode = false; /** - * Avoid all cached db reads; open inbound.db read-only with mmap and page cache disabled. - * + * Avoid all cached db reads; open inbound.db read-only with mmap and page cache disabled. + * * Use this (not getInboundDb) for readers that need to see host-written rows * promptly — e.g. messages_in polling. Caller must .close() the returned * connection (try/finally). * * Needed for mounts where host writes don't reliably invalidate * SQLite's caches: virtiofs (Colima, Lima, Podman Machine, Apple - * Container), NFS. - * + * Container), NFS. + * * Cost is microseconds per query, so safe for universal use. */ export function openInboundDb(): Database { + // In test mode return a thin wrapper over the in-memory singleton. + // Callers do try/finally { db.close() } — the wrapper no-ops close() + // so the singleton survives for the rest of the test. + if (_testMode && _inbound) { + const db = _inbound; + return { prepare: (sql: string) => db.prepare(sql), exec: (sql: string) => db.exec(sql), close: () => {} } as unknown as Database; + } const db = new Database(DEFAULT_INBOUND_PATH, { readonly: true }); db.exec('PRAGMA busy_timeout = 5000'); db.exec('PRAGMA mmap_size = 0'); @@ -170,6 +178,7 @@ export function clearStaleProcessingAcks(): void { /** For tests — creates in-memory DBs with the session schemas. */ export function initTestSessionDb(): { inbound: Database; outbound: Database } { + _testMode = true; _inbound = new Database(':memory:'); _inbound.exec('PRAGMA foreign_keys = ON'); _inbound.exec(` @@ -246,6 +255,7 @@ export function initTestSessionDb(): { inbound: Database; outbound: Database } { export function closeSessionDb(): void { _inbound?.close(); _inbound = null; + _testMode = false; _outbound?.close(); _outbound = null; } diff --git a/nanoclaw.sh b/nanoclaw.sh index c17966e..bcf4e49 100755 --- a/nanoclaw.sh +++ b/nanoclaw.sh @@ -138,16 +138,13 @@ write_header cat "$PROJECT_ROOT/assets/setup-splash.txt" # ─── pre-flight: minimum hardware specs ──────────────────────────────── -# NanoClaw runs an agent container per session. Below these thresholds the -# host + container + agent will struggle (OOM under load, image + session -# DBs filling the disk). Soft warn — `df` only sees the partition that -# $PROJECT_ROOT lives on, which can underreport on hosts with separate -# /home or /var mounts, so the user can override. +# NanoClaw runs an agent container per session. Below this threshold the +# host + container + agent will struggle (OOM under load). Soft warn — the +# user can override. # RAM floor is set below 4 GB because "4 GB" VMs typically report 3700–3900 MB # after kernel reserves (e.g. Hetzner CX21 ≈ 3814, AWS t3.medium ≈ 3800). MIN_MEM_MB=3700 -MIN_DISK_GB=20 detect_mem_mb() { case "$(uname -s)" in @@ -162,39 +159,29 @@ detect_mem_mb() { esac } -detect_disk_gb() { - # -P: POSIX format (no line-wrapping); -k: 1024-byte blocks. Avail is col 4. - df -Pk "$PROJECT_ROOT" 2>/dev/null \ - | awk 'NR==2 { printf "%d", $4 / 1024 / 1024 }' -} - MEM_MB=$(detect_mem_mb) -DISK_GB=$(detect_disk_gb) : "${MEM_MB:=0}" -: "${DISK_GB:=0}" -LOW_MEM=false; LOW_DISK=false -[ "$MEM_MB" -gt 0 ] && [ "$MEM_MB" -lt "$MIN_MEM_MB" ] && LOW_MEM=true -[ "$DISK_GB" -gt 0 ] && [ "$DISK_GB" -lt "$MIN_DISK_GB" ] && LOW_DISK=true +LOW_MEM=false +[ "$MEM_MB" -gt 0 ] && [ "$MEM_MB" -lt "$MIN_MEM_MB" ] && LOW_MEM=true -if [ "$LOW_MEM" = true ] || [ "$LOW_DISK" = true ]; then +if [ "$LOW_MEM" = true ]; then printf ' %s\n' "$(red 'Warning: this machine likely cannot run NanoClaw.')" - printf ' %s\n' "$(dim 'NanoClaw recommends a 4 GB+ machine with 20 GB+ free disk. Below this,')" - printf ' %s\n' "$(dim 'the host + agent container will run out of memory or disk under most')" - printf ' %s\n' "$(dim 'workloads. A stronger machine is strongly recommended.')" - [ "$LOW_MEM" = true ] && printf ' %s\n' "$(dim " · Detected RAM: ${MEM_MB} MB")" - [ "$LOW_DISK" = true ] && printf ' %s\n' "$(dim " · Free disk on $PROJECT_ROOT: ${DISK_GB} GB")" + printf ' %s\n' "$(dim 'NanoClaw recommends a 4 GB+ RAM machine. Below this, the host + agent')" + printf ' %s\n' "$(dim 'container will run out of memory under most workloads. A stronger')" + printf ' %s\n' "$(dim 'machine is strongly recommended.')" + printf ' %s\n' "$(dim " · Detected RAM: ${MEM_MB} MB")" printf '\n' read -r -p " $(bold 'Try anyway?') [y/N] " SPECS_ANS - 140k tokens, 70% of context window + + 141k tokens, 70% of context window @@ -15,8 +15,8 @@ tokens - - 140k + + 141k diff --git a/setup/install-node.sh b/setup/install-node.sh index e100ccd..4ecb1c5 100755 --- a/setup/install-node.sh +++ b/setup/install-node.sh @@ -17,30 +17,40 @@ if command -v node >/dev/null 2>&1; then exit 0 fi -case "$(uname -s)" in - Darwin) - echo "STEP: brew-install-node" - if ! command -v brew >/dev/null 2>&1; then +if command -v uvx >/dev/null 2>&1; then + echo "STEP: uvx-nodeenv" + uvx nodeenv -n lts ~/node + mkdir -p ~/.local/bin + ln -sf ~/node/bin/node ~/.local/bin/node + ln -sf ~/node/bin/npm ~/.local/bin/npm + ln -sf ~/node/bin/npx ~/.local/bin/npx + ln -sf ~/node/bin/pnpm ~/.local/bin/pnpm +else + case "$(uname -s)" in + Darwin) + echo "STEP: brew-install-node" + if ! command -v brew >/dev/null 2>&1; then + echo "STATUS: failed" + echo "ERROR: Homebrew not installed. Install brew first (https://brew.sh) then re-run." + echo "=== END ===" + exit 1 + fi + brew install node@22 + ;; + Linux) + echo "STEP: nodesource-setup" + curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - + echo "STEP: apt-install-nodejs" + sudo apt-get install -y nodejs + ;; + *) echo "STATUS: failed" - echo "ERROR: Homebrew not installed. Install brew first (https://brew.sh) then re-run." + echo "ERROR: Unsupported platform: $(uname -s)" echo "=== END ===" exit 1 - fi - brew install node@22 - ;; - Linux) - echo "STEP: nodesource-setup" - curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - - echo "STEP: apt-install-nodejs" - sudo apt-get install -y nodejs - ;; - *) - echo "STATUS: failed" - echo "ERROR: Unsupported platform: $(uname -s)" - echo "=== END ===" - exit 1 - ;; -esac + ;; + esac +fi if ! command -v node >/dev/null 2>&1; then echo "STATUS: failed" diff --git a/src/host-sweep.ts b/src/host-sweep.ts index 09c82ac..93a7e87 100644 --- a/src/host-sweep.ts +++ b/src/host-sweep.ts @@ -256,7 +256,7 @@ export function _resetStuckProcessingRowsForTesting( session: Session, reason: string, ): void { - resetStuckProcessingRows(inDb, outDb, session, reason); + resetStuckProcessingRows(inDb, outDb, session, reason, outDb); } function resetStuckProcessingRows( @@ -264,6 +264,7 @@ function resetStuckProcessingRows( outDb: Database.Database, session: Session, reason: string, + writableOutDb?: Database.Database, ): void { const claims = getProcessingClaims(outDb); const now = Date.now(); @@ -300,19 +301,17 @@ function resetStuckProcessingRows( // would re-read them, see the old status_changed timestamp, conclude the // freshly respawned container is stuck, and SIGKILL it before its // agent-runner has a chance to run clearStaleProcessingAcks() on startup. - // We're safe to write outbound.db here because we just killed the container - // that owned it (or it crashed and left no writer behind). - // outDb was opened readonly for reads above; reopen with write access for this delete. - let outDbRw: Database.Database | null = null; + const ownsDb = !writableOutDb; + let useDb: Database.Database | null = writableOutDb ?? null; try { - outDbRw = openOutboundDbRw(session.agent_group_id, session.id); - const cleared = deleteOrphanProcessingClaims(outDbRw); + if (!useDb) useDb = openOutboundDbRw(session.agent_group_id, session.id); + const cleared = deleteOrphanProcessingClaims(useDb); if (cleared > 0) { log.info('Cleared orphan processing claims', { sessionId: session.id, cleared, reason }); } } catch (err) { log.warn('Failed to clear orphan processing claims', { sessionId: session.id, err }); } finally { - outDbRw?.close(); + if (ownsDb) useDb?.close(); } }