Merge branch 'main' into fix/drop-whatsapp-lid-migration
This commit is contained in:
@@ -27,6 +27,7 @@ const DEFAULT_HEARTBEAT_PATH = '/workspace/.heartbeat';
|
|||||||
let _inbound: Database | null = null;
|
let _inbound: Database | null = null;
|
||||||
let _outbound: Database | null = null;
|
let _outbound: Database | null = null;
|
||||||
let _heartbeatPath: string = DEFAULT_HEARTBEAT_PATH;
|
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.
|
||||||
@@ -42,6 +43,13 @@ let _heartbeatPath: string = DEFAULT_HEARTBEAT_PATH;
|
|||||||
* Cost is microseconds per query, so safe for universal use.
|
* Cost is microseconds per query, so safe for universal use.
|
||||||
*/
|
*/
|
||||||
export function openInboundDb(): Database {
|
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 });
|
const db = new Database(DEFAULT_INBOUND_PATH, { readonly: true });
|
||||||
db.exec('PRAGMA busy_timeout = 5000');
|
db.exec('PRAGMA busy_timeout = 5000');
|
||||||
db.exec('PRAGMA mmap_size = 0');
|
db.exec('PRAGMA mmap_size = 0');
|
||||||
@@ -170,6 +178,7 @@ export function clearStaleProcessingAcks(): void {
|
|||||||
|
|
||||||
/** For tests — creates in-memory DBs with the session schemas. */
|
/** For tests — creates in-memory DBs with the session schemas. */
|
||||||
export function initTestSessionDb(): { inbound: Database; outbound: Database } {
|
export function initTestSessionDb(): { inbound: Database; outbound: Database } {
|
||||||
|
_testMode = true;
|
||||||
_inbound = new Database(':memory:');
|
_inbound = new Database(':memory:');
|
||||||
_inbound.exec('PRAGMA foreign_keys = ON');
|
_inbound.exec('PRAGMA foreign_keys = ON');
|
||||||
_inbound.exec(`
|
_inbound.exec(`
|
||||||
@@ -246,6 +255,7 @@ export function initTestSessionDb(): { inbound: Database; outbound: Database } {
|
|||||||
export function closeSessionDb(): void {
|
export function closeSessionDb(): void {
|
||||||
_inbound?.close();
|
_inbound?.close();
|
||||||
_inbound = null;
|
_inbound = null;
|
||||||
|
_testMode = false;
|
||||||
_outbound?.close();
|
_outbound?.close();
|
||||||
_outbound = null;
|
_outbound = null;
|
||||||
}
|
}
|
||||||
|
|||||||
39
nanoclaw.sh
39
nanoclaw.sh
@@ -138,16 +138,13 @@ write_header
|
|||||||
cat "$PROJECT_ROOT/assets/setup-splash.txt"
|
cat "$PROJECT_ROOT/assets/setup-splash.txt"
|
||||||
|
|
||||||
# ─── pre-flight: minimum hardware specs ────────────────────────────────
|
# ─── pre-flight: minimum hardware specs ────────────────────────────────
|
||||||
# NanoClaw runs an agent container per session. Below these thresholds the
|
# NanoClaw runs an agent container per session. Below this threshold the
|
||||||
# host + container + agent will struggle (OOM under load, image + session
|
# host + container + agent will struggle (OOM under load). Soft warn — the
|
||||||
# DBs filling the disk). Soft warn — `df` only sees the partition that
|
# user can override.
|
||||||
# $PROJECT_ROOT lives on, which can underreport on hosts with separate
|
|
||||||
# /home or /var mounts, so the user can override.
|
|
||||||
|
|
||||||
# RAM floor is set below 4 GB because "4 GB" VMs typically report 3700–3900 MB
|
# 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).
|
# after kernel reserves (e.g. Hetzner CX21 ≈ 3814, AWS t3.medium ≈ 3800).
|
||||||
MIN_MEM_MB=3700
|
MIN_MEM_MB=3700
|
||||||
MIN_DISK_GB=20
|
|
||||||
|
|
||||||
detect_mem_mb() {
|
detect_mem_mb() {
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
@@ -162,39 +159,29 @@ detect_mem_mb() {
|
|||||||
esac
|
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)
|
MEM_MB=$(detect_mem_mb)
|
||||||
DISK_GB=$(detect_disk_gb)
|
|
||||||
: "${MEM_MB:=0}"
|
: "${MEM_MB:=0}"
|
||||||
: "${DISK_GB:=0}"
|
|
||||||
|
|
||||||
LOW_MEM=false; LOW_DISK=false
|
LOW_MEM=false
|
||||||
[ "$MEM_MB" -gt 0 ] && [ "$MEM_MB" -lt "$MIN_MEM_MB" ] && LOW_MEM=true
|
[ "$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
|
|
||||||
|
|
||||||
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' "$(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 'NanoClaw recommends a 4 GB+ RAM machine. Below this, the host + agent')"
|
||||||
printf ' %s\n' "$(dim 'the host + agent container will run out of memory or disk under most')"
|
printf ' %s\n' "$(dim 'container will run out of memory under most workloads. A stronger')"
|
||||||
printf ' %s\n' "$(dim 'workloads. A stronger machine is strongly recommended.')"
|
printf ' %s\n' "$(dim 'machine is strongly recommended.')"
|
||||||
[ "$LOW_MEM" = true ] && printf ' %s\n' "$(dim " · Detected RAM: ${MEM_MB} MB")"
|
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 '\n'
|
printf '\n'
|
||||||
read -r -p " $(bold 'Try anyway?') [y/N] " SPECS_ANS </dev/tty
|
read -r -p " $(bold 'Try anyway?') [y/N] " SPECS_ANS </dev/tty
|
||||||
|
|
||||||
case "${SPECS_ANS:-N}" in
|
case "${SPECS_ANS:-N}" in
|
||||||
[Yy]*)
|
[Yy]*)
|
||||||
ph_event setup_low_specs_continued mem_mb="$MEM_MB" disk_gb="$DISK_GB" low_mem="$LOW_MEM" low_disk="$LOW_DISK"
|
ph_event setup_low_specs_continued mem_mb="$MEM_MB" low_mem="$LOW_MEM"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
ph_event setup_low_specs_aborted mem_mb="$MEM_MB" disk_gb="$DISK_GB" low_mem="$LOW_MEM" low_disk="$LOW_DISK"
|
ph_event setup_low_specs_aborted mem_mb="$MEM_MB" low_mem="$LOW_MEM"
|
||||||
printf '\n %s\n\n' "$(dim 'Aborted. Re-run after upgrading the host or freeing disk space.')"
|
printf '\n %s\n\n' "$(dim 'Aborted. Re-run after upgrading the host.')"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nanoclaw",
|
"name": "nanoclaw",
|
||||||
"version": "2.0.30",
|
"version": "2.0.31",
|
||||||
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
|
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.33.0",
|
"packageManager": "pnpm@10.33.0",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="140k tokens, 70% of context window">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="141k tokens, 70% of context window">
|
||||||
<title>140k tokens, 70% of context window</title>
|
<title>141k tokens, 70% of context window</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
||||||
<text aria-hidden="true" x="26" y="15" fill="#010101" fill-opacity=".3">tokens</text>
|
<text aria-hidden="true" x="26" y="15" fill="#010101" fill-opacity=".3">tokens</text>
|
||||||
<text x="26" y="14">tokens</text>
|
<text x="26" y="14">tokens</text>
|
||||||
<text aria-hidden="true" x="71" y="15" fill="#010101" fill-opacity=".3">140k</text>
|
<text aria-hidden="true" x="71" y="15" fill="#010101" fill-opacity=".3">141k</text>
|
||||||
<text x="71" y="14">140k</text>
|
<text x="71" y="14">141k</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -17,30 +17,40 @@ if command -v node >/dev/null 2>&1; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$(uname -s)" in
|
if command -v uvx >/dev/null 2>&1; then
|
||||||
Darwin)
|
echo "STEP: uvx-nodeenv"
|
||||||
echo "STEP: brew-install-node"
|
uvx nodeenv -n lts ~/node
|
||||||
if ! command -v brew >/dev/null 2>&1; then
|
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 "STATUS: failed"
|
||||||
echo "ERROR: Homebrew not installed. Install brew first (https://brew.sh) then re-run."
|
echo "ERROR: Unsupported platform: $(uname -s)"
|
||||||
echo "=== END ==="
|
echo "=== END ==="
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
;;
|
||||||
brew install node@22
|
esac
|
||||||
;;
|
fi
|
||||||
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
|
|
||||||
|
|
||||||
if ! command -v node >/dev/null 2>&1; then
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
echo "STATUS: failed"
|
echo "STATUS: failed"
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export function _resetStuckProcessingRowsForTesting(
|
|||||||
session: Session,
|
session: Session,
|
||||||
reason: string,
|
reason: string,
|
||||||
): void {
|
): void {
|
||||||
resetStuckProcessingRows(inDb, outDb, session, reason);
|
resetStuckProcessingRows(inDb, outDb, session, reason, outDb);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetStuckProcessingRows(
|
function resetStuckProcessingRows(
|
||||||
@@ -264,6 +264,7 @@ function resetStuckProcessingRows(
|
|||||||
outDb: Database.Database,
|
outDb: Database.Database,
|
||||||
session: Session,
|
session: Session,
|
||||||
reason: string,
|
reason: string,
|
||||||
|
writableOutDb?: Database.Database,
|
||||||
): void {
|
): void {
|
||||||
const claims = getProcessingClaims(outDb);
|
const claims = getProcessingClaims(outDb);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -300,19 +301,17 @@ function resetStuckProcessingRows(
|
|||||||
// would re-read them, see the old status_changed timestamp, conclude the
|
// would re-read them, see the old status_changed timestamp, conclude the
|
||||||
// freshly respawned container is stuck, and SIGKILL it before its
|
// freshly respawned container is stuck, and SIGKILL it before its
|
||||||
// agent-runner has a chance to run clearStaleProcessingAcks() on startup.
|
// agent-runner has a chance to run clearStaleProcessingAcks() on startup.
|
||||||
// We're safe to write outbound.db here because we just killed the container
|
const ownsDb = !writableOutDb;
|
||||||
// that owned it (or it crashed and left no writer behind).
|
let useDb: Database.Database | null = writableOutDb ?? null;
|
||||||
// outDb was opened readonly for reads above; reopen with write access for this delete.
|
|
||||||
let outDbRw: Database.Database | null = null;
|
|
||||||
try {
|
try {
|
||||||
outDbRw = openOutboundDbRw(session.agent_group_id, session.id);
|
if (!useDb) useDb = openOutboundDbRw(session.agent_group_id, session.id);
|
||||||
const cleared = deleteOrphanProcessingClaims(outDbRw);
|
const cleared = deleteOrphanProcessingClaims(useDb);
|
||||||
if (cleared > 0) {
|
if (cleared > 0) {
|
||||||
log.info('Cleared orphan processing claims', { sessionId: session.id, cleared, reason });
|
log.info('Cleared orphan processing claims', { sessionId: session.id, cleared, reason });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn('Failed to clear orphan processing claims', { sessionId: session.id, err });
|
log.warn('Failed to clear orphan processing claims', { sessionId: session.id, err });
|
||||||
} finally {
|
} finally {
|
||||||
outDbRw?.close();
|
if (ownsDb) useDb?.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user