Several async calls in the message loop and group queue are
fire-and-forget without .catch() handlers. When WhatsApp disconnects
or containers fail unexpectedly, these produce unhandled rejections
that can crash the process.
Add explicit .catch() at each call site so errors are logged with
full context (groupJid, taskId) instead of crashing:
- channel.setTyping() in message loop (adapted for channel abstraction)
- startMessageLoop() in main()
- runForGroup() and runTask() in group-queue (5 call sites)
Closes#221
Co-authored-by: Naveen Jain <1779929+naveenspark@users.noreply.github.com>
Co-authored-by: Skip Potter <skip.potter.va@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The test expected voice notes (audioMessage with no caption) to be
delivered with empty content, but 6f177ad added a guard that skips
messages with no text content. Update assertion accordingly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The HOME_DIR fallback '/Users/user' is macOS-specific and incorrect.
Use os.homedir() from Node's os module which works cross-platform
and returns the actual home directory from /etc/passwd on Linux.
Co-authored-by: Cursor <cursoragent@cursor.com>
The original notifyIdle condition (!result.result) never fired in
streaming input mode because every result has non-null text content.
This caused due tasks to wait up to 30 minutes for the idle timer.
- Call notifyIdle for ALL successful results (not just null ones)
- Add isTaskContainer flag so user messages queue instead of being
forwarded to task containers (which blocked notifyIdle from the
message container's onOutput path)
- Reset idleWaiting in sendMessage so containers aren't preempted
while actively working on a new incoming message
- Replace 30-min IDLE_TIMEOUT with 10s close timer for task containers
since they are single-turn and should exit promptly after their result
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Containers that finish work but stay alive in waitForIpcMessage() block
queued scheduled tasks. Previous approaches killed active containers
mid-work. This fix tracks idle state via the session-update marker
(status: success, result: null) and only preempts when the container
is idle-waiting, not actively working.
Closes#293
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WhatsApp protocol messages (encryption key distribution, read receipts,
ephemeral settings, senderKeyDistributionMessage) have a message envelope
but no text content. These were being stored in messages.db and processed
by the agent, causing:
1. Agent responds to empty messages, wasting API tokens
2. Container stays alive indefinitely (idle timer resets)
3. Scheduled tasks blocked (queue slot occupied)
This fix skips messages with empty content before calling onMessage,
preventing protocol messages from being stored and processed.
Fixes#250
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
copyFileSync crashes with EISDIR when a skill contains subdirectories
like scripts/. Skills support nested folders (scripts/, examples/,
templates/) per the Claude Code spec. Use fs.cpSync to handle the
complete skill structure.
* Fix trigger pattern tests to use config name
Tests hardcoded "Andy" but the pattern is built from
`ASSISTANT_NAME` which comes from `.env`.
---
Generated with the help of Claude Code, https://claude.ai/code
Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
* Restore usage comment in trim test
---
Generated with the help of Claude Code, https://claude.ai/code
Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Code Opus 4.6 <noreply@anthropic.com>
- Fix 你→您 inconsistency in README_zh.md
- Add missing nanoclaw.dev link to README_zh.md header
- Remove Skills System CLI section from both README.md and README_zh.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cleanupMergeState() without a filePath runs bare `git reset`, which
resets the entire index and can stage deletions of unrelated files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Docker is now the default runtime. The /convert-to-apple-container skill
uses the new skills engine format (manifest.yaml, modify/, intent files,
tests/) to switch to Apple Container on macOS.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add voice transcription skill package at
.claude/skills/add-voice-transcription/ so it can be applied via the
skills engine. Skill adds src/transcription.ts (OpenAI Whisper), modifies
whatsapp.ts to detect/transcribe voice notes, and includes intent files,
3 test cases, and 8 skill validation tests.
Also fixes skills engine runNpmInstall() to use --legacy-peer-deps,
needed for any skill adding deps with Zod v3 peer requirements.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
All skills now reference Docker CLI instead of Apple Container CLI.
Setup skill defaults to Docker with optional /convert-to-apple-container.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Move all container-runtime-specific logic (binary name, mount args,
stop command, startup check, orphan cleanup) into a single file so
swapping runtimes only requires replacing this one file.
Neutralize "Apple Container" references in comments and docs that
would become incorrect after a runtime swap. References that list
both runtimes as options are left unchanged.
No behavior change — Apple Container remains the default runtime.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: multi-channel infrastructure with explicit channel/is_group tracking
- Add channels[] array and findChannel() routing in index.ts, replacing
hardcoded whatsapp.* calls with channel-agnostic callbacks
- Add channel TEXT and is_group INTEGER columns to chats table with
COALESCE upsert to protect existing values from null overwrites
- is_group defaults to 0 (safe: unknown chats excluded from groups)
- WhatsApp passes explicit channel='whatsapp' and isGroup to onChatMetadata
- getAvailableGroups filters on is_group instead of JID pattern matching
- findChannel logs warnings instead of silently dropping unroutable JIDs
- Migration backfills channel/is_group from JID patterns for existing DBs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: skills engine v0.1 — deterministic skill packages with rerere resolution
Three-way merge engine for applying skill packages on top of a core
codebase. Skills declare which files they add/modify, and the engine
uses git merge-file for conflict detection with git rerere for
automatic resolution of previously-seen conflicts.
Key components:
- apply: three-way merge with backup/rollback safety net
- replay: clean-slate replay for uninstall and rebase
- update: core version updates with deletion detection
- rebase: bake applied skills into base (one-way)
- manifest: validation with path traversal protection
- resolution-cache: pre-computed rerere resolutions
- structured: npm deps, env vars, docker-compose merging
- CI: per-skill test matrix with conflict detection
151 unit tests covering merge, rerere, backup, replay, uninstall,
update, rebase, structured ops, and edge cases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add Discord and Telegram skill packages
Skill packages for adding Discord and Telegram channels to NanoClaw.
Each package includes:
- Channel implementation (add/src/channels/)
- Three-way merge targets for index.ts, config.ts, routing.test.ts
- Intent docs explaining merge invariants
- Standalone integration tests
- manifest.yaml with dependency/conflict declarations
Applied via: npx tsx scripts/apply-skill.ts .claude/skills/add-discord
These are inert until applied — no runtime impact.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* remove unused docs (skills-system-status, implementation-guide)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Use pipe delimiter in sed and quote the value to prevent breakage
from names containing /, &, \, or spaces.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: WA 515 stream error reconnect exiting early before key sync
Pass isReconnect flag on 515 reconnect so the registered-creds check
doesn't bail out before the handshake completes (caused "logging in..."
hang after successful pairing).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: container permission errors on Docker with non-default uid
Make /home/node world-writable in the Dockerfile so the SDK can write
.claude.json. Add --user flag matching host uid/gid in container-runner
so bind-mounted files are accessible. Skip when running as root (uid 0),
as the container's node user (uid 1000), or on native Windows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: write ASSISTANT_NAME to .env during setup
When a custom assistant name is chosen, persist it to .env so config.ts
picks it up at runtime. Uses temp file for cross-platform sed
compatibility (macOS/Linux/WSL).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The setup skill would skip the /convert-to-docker step because:
1. The conversion instructions were buried inline in runtime choice bullets
2. The convert-to-docker skill had disable-model-invocation: true
Restructured step 3 into explicit sub-steps with a hard gate (3b) that
checks source files for Apple Container references before allowing the
build to proceed. Enabled model invocation on the convert-to-docker skill.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline SKILL.md instructions with executable shell scripts
for each setup phase (environment check, deps, container, auth,
groups, channels, mounts, service, verify). Scripts emit structured
status blocks for reliable parsing.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Switches from default GITHUB_TOKEN to a scoped GitHub App token
so the workflow can push to the protected main branch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add is_bot_message column and support dedicated phone numbers
Replace fragile content-prefix bot detection with an explicit
is_bot_message database column. The old prefix check (content NOT LIKE
'Andy:%') is kept as a backstop for pre-migration messages.
- Add is_bot_message column with automatic backfill migration
- Add ASSISTANT_HAS_OWN_NUMBER env var to skip name prefix when the
assistant has its own WhatsApp number
- Move prefix logic into WhatsApp channel (no longer a router concern)
- Remove prefixAssistantName from Channel interface
- Load .env via dotenv so launchd-managed processes pick up config
- WhatsApp bot detection: fromMe for own number, prefix match for shared
Based on #160 and #173.
Co-Authored-By: Stefan Gasser <stefan@stefangasser.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract shared .env parser and remove dotenv dependency
Extract .env parsing into src/env.ts, used by both config.ts and
container-runner.ts. Reads only requested keys without loading secrets
into process.env, avoiding leaking API keys to child processes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Stefan Gasser <stefan@stefangasser.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reusable composite action that counts codebase tokens using tiktoken
and generates a shields.io-style SVG badge. Color reflects context
window usage: green (<30%), yellow-green (30-50%), yellow (50-70%),
red (70%+). Badge includes hardcoded link back to repo-tokens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues fixed:
- Use 'paused' instead of 'available' to stop typing. Baileys'
sendPresenceUpdate('available') sends a global <presence> stanza and
ignores the JID, so chatstate never left 'composing' and WhatsApp
suppressed duplicate composing notifications per XEP-0085.
- Add setTyping call when piping messages to an already-running
container. Previously only the first message (which spawns a new
container) triggered the typing indicator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added missing Browsers mock to the Baileys vi.mock and made
triggerMessages async to flush microtasks before assertions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sending 'paused' after the first response caused WhatsApp to stop
relaying subsequent 'composing' presence updates. Using 'available'
keeps the bot in a state where typing indicators work consistently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without announcing 'available' after connecting, WhatsApp stops relaying
composing/paused presence updates after the first message.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass secrets to the SDK via the `env` query option instead of setting
process.env, so Bash subprocesses never inherit API keys. Delete
/tmp/input.json immediately after reading to remove secrets from disk.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Use a PreToolUse SDK hook to prepend `unset ANTHROPIC_API_KEY
CLAUDE_CODE_OAUTH_TOKEN` to every Bash command Kit runs, preventing
secret leakage via env/printenv/echo/$PROC. Secrets are now passed
via stdin JSON instead of mounted env files, closing all known
exfiltration vectors.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Move the language switcher from above the logo into the badge row
alongside Discord. Update README_zh.md to match current English
README: add Agent Swarms announcement and feature, update
architecture section with all key files, add Community section,
and fix file descriptions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- IPC register_group handler now passes requiresTrigger field to registerGroup(),
fixing groups silently defaulting to trigger-required mode
- Agent runner scans /workspace/extra/* and passes them as additionalDirectories
to the SDK query, so CLAUDE.md files in mounted dirs are loaded automatically
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>