Commit Graph

41 Commits

Author SHA1 Message Date
gavrielc
03df69e9b5 fix: address review feedback for per-group queue reliability
- Fix startup recovery running before WhatsApp connects, which could
  permanently lose agent responses by advancing lastAgentTimestamp
  before sock is initialized
- Add 5s retry on container failure so messages aren't silently dropped
  until a new message arrives for the group
- Use `container stop` in shutdown instead of raw SIGTERM to CLI wrapper,
  ensuring proper container cleanup
- Replace unnecessary dynamic imports with static imports in processTaskIpc
- Guard JSON.parse of DB-stored last_agent_timestamp against corruption
- Validate MAX_CONCURRENT_CONTAINERS (default 5, min 1, NaN-safe)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 16:45:00 +02:00
gavrielc
eac9a6acfd feat: per-group queue, SQLite state, graceful shutdown
Add per-group container locking with global concurrency limit to prevent
concurrent containers for the same group (#89) and cap total containers.
Fix message batching bug where lastAgentTimestamp advanced to trigger
message instead of latest in batch, causing redundant re-processing.
Move router state, sessions, and registered groups from JSON files to
SQLite with automatic one-time migration. Add SIGTERM/SIGINT handlers
with graceful shutdown (SIGTERM -> grace period -> SIGKILL). Add startup
recovery for messages missed during crash. Remove dead code: utils.ts,
Session type, isScheduledTask flag, ContainerConfig.env, getTaskRunLogs,
GroupQueue.isActive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 07:38:07 +02:00
gavrielc
db216a459e fix: proper container lifecycle management to prevent stopped container accumulation
- Name containers (nanoclaw-{group}-{timestamp}) for trackability
- Replace SIGKILL timeout with graceful `container stop` so --rm fires
- Add startup sweep to clean up stopped nanoclaw containers from previous runs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 07:10:26 +02:00
Gavriel Cohen
3a4d340f80 Fix duplicate responses caused by reconnect-stacking loops
WhatsApp reconnections called startMessageLoop/startSchedulerLoop/
startIpcWatcher and setInterval again without stopping the previous
instances, creating parallel loops that processed the same messages.

Add guard flags so each loop starts only once per process lifetime.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:18:55 +02:00
Ejae-dev
117980175e refactor: deduplicate logger into shared module (#39)
three files created identical pino logger instances with the same config.
extract into src/logger.ts and import from each consumer.

net -9 lines, no behavior change.

Co-authored-by: ejae <ejae_dev@ejaes-Mac-mini.home>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 00:40:58 +02:00
yingchao
392ba6262c fix: translate WhatsApp LID JIDs to phone JIDs for self-chat messages (#62)
WhatsApp recently changed to send self-chat messages using LID (Linked
ID) format (e.g., xxxxxx@lid) instead of phone number format (e.g.,
xxxxxx@s.whatsapp.net). This caused messages to yourself to be silently
dropped because they didn't match any registered group.

## How to reproduce
1. Send a message to yourself on WhatsApp with the trigger
2. Message is received by Baileys but remoteJid is in LID format
3. LID JID doesn't match registered group JID (phone format)
4. Message is not stored and no response is sent

## The fix
- Build a LID-to-phone mapping from sock.user on connection open
- Translate incoming LID JIDs to phone JIDs before storing/processing messages
- This allows self-chat messages to correctly match the registered main channel

The mapping is populated from sock.user.id (phone) and sock.user.lid (LID)
which Baileys provides after successful authentication.
2026-02-04 00:33:50 +02:00
gavrielc
21c66df2b1 Add prettier
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 17:14:17 +02:00
Gavriel
4711ec435a Add register_group IPC command for dynamic group registration
Main agent can now register new groups via MCP tool without restart.
Host updates both in-memory state and JSON file, creates group folders.
Authorization enforced at both agent and host level.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 00:08:40 +02:00
gavrielc
33ef0c68d3 Fix message cursor to only advance on successful processing (#17)
Previously, lastTimestamp was unconditionally advanced after each message,
even if processMessage failed. This caused transient errors to permanently
drop messages since they would never be retried.

Now the cursor only advances after successful processing, implementing
at-least-once delivery semantics. On failure, the loop breaks and the
failed message will be retried on the next poll iteration.

https://claude.ai/code/session_01SEQDWxXeZHe7t1bb5cw2CA

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-01 23:05:37 +02:00
gavrielc
df52232763 Pre-launch fixes: error handling, cleanup, consistency
- Remove unused claude-agent-sdk from host deps (only used in container)
- Remove dead scheduler MCP config (built into IPC)
- Remove unused eslint script
- Add clear error message when Apple Container fails to start
- Auto-generate launchd plist with real paths in setup skill
- Standardize Node.js version to 20+ everywhere

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:01:04 +02:00
Gavriel
5760b75fa9 Fix timezone handling and message filtering
- Add TIMEZONE config using system timezone for cron expressions
- Filter bot messages by content prefix instead of is_from_me
  (user shares WhatsApp account with bot)
- Format messages as XML for cleaner agent parsing
- Update schedule_task tool to clarify local time usage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 22:54:44 +02:00
gavrielc
016a1a0e31 Add group metadata sync for easier group activation
- Sync group names from WhatsApp via groupFetchAllParticipating()
- Store group names in chats table (jid -> name mapping)
- Daily sync with 24h cache, on-demand refresh via IPC
- Write available_groups.json snapshot for agent (main group only)
- Agent can request refresh_groups via IPC if group not found
- Update documentation in main CLAUDE.md and debug skill

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 22:25:29 +02:00
Gavriel
572338b9a6 Add context_mode option for scheduled tasks
Scheduled tasks can now run in either:
- "group" mode: uses the group's conversation session for context
- "isolated" mode: runs with a fresh session (previous behavior)

The tool description guides the agent on when to use each mode and
prompts them to ask the user if unclear. Group mode is now the default.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 22:23:50 +02:00
gavrielc
f6e7f7aca9 Make main group respond to all messages without trigger prefix
Other groups still require @Andy prefix to trigger the agent.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:49:39 +02:00
gavrielc
6745a1c54b Apply fixes from closed PRs: sentinel markers, JID lookup, schedule validation
- PR #10: Add sentinel markers for robust JSON parsing between container
  and host. Fallback to last-line parsing for backwards compatibility.

- PR #5: Look up target JID from registeredGroups instead of trusting
  IPC payload, fixing cross-group scheduled tasks getting wrong chat_jid.

- PR #8: Add lightweight schedule validation in container MCP that
  returns errors to agents (cron syntax, positive interval, valid ISO
  timestamp). Also defensive validation on host side.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 20:49:57 +02:00
gavrielc
ade9f2d323 Merge pull request #3 from gavrielc/claude/secure-ipc-access-Ni9l4
Secure IPC with per-group namespaces to prevent privilege escalation
2026-02-01 20:40:27 +02:00
gavrielc
febf90f3c8 Merge pull request #12 from gavrielc/claude/fix-agent-failure-timestamp-yiOZt
Fix: only update lastAgentTimestamp on agent success
2026-02-01 20:40:15 +02:00
gavrielc
8f89f67c4b Merge pull request #11 from gavrielc/claude/fix-message-loss-error-DJwye
Fix message loss when processMessage throws
2026-02-01 20:40:13 +02:00
Claude
7aa051fa8a Fix: only update lastAgentTimestamp on agent success
Previously, lastAgentTimestamp was updated regardless of whether
runAgent succeeded or failed. This caused messages to be skipped
on retry after a failure since they were already marked as processed.

Now the timestamp is only advanced when the agent returns a valid
response, ensuring failed messages will be included in the next retry.

https://claude.ai/code/session_0118wNAJCKY2Asr17Lb91TE2
2026-02-01 18:13:34 +00:00
Claude
a904c65975 Fix message loss when processMessage throws
Previously, lastTimestamp was advanced before processing messages.
If processMessage threw an error, those messages would be skipped
on the next tick since the timestamp had already moved past them.

Now we advance lastTimestamp after each message is successfully
processed (or after catching an error), ensuring no messages are
lost on retry.

https://claude.ai/code/session_01LgHSAs9XbcvZckCe8xPipj
2026-02-01 18:12:59 +00:00
Claude
092411dd22 Remove message content from info-level logs
Previously logged first 50 chars of message text at info level, which
could expose sensitive user content in production logs. Now logs only
message length instead.

https://claude.ai/code/session_01V9b7PCAVA7LoyrR89t3GTG
2026-02-01 17:58:57 +00:00
Claude
6a94aec5da Secure IPC with per-group namespaces to prevent privilege escalation
Each container now gets its own IPC directory (/data/ipc/{groupFolder}/)
instead of a shared global directory. Identity is determined by which
directory a request came from, not by self-reported data in IPC files.

Authorization enforced:
- send_message: only to chatJids belonging to the source group
- schedule_task: only for the source group (main can target any)
- pause/resume/cancel_task: only for tasks owned by source group

https://claude.ai/code/session_018nmxNEbtgJH7cKDyBSQGAw
2026-02-01 17:44:25 +00:00
gavrielc
17e7b469f4 Refactor: delete dead code, extract utils, rename files for clarity
- Delete scheduler-mcp.ts (285 lines of dead code, unused)
- Extract loadJson/saveJson to utils.ts (generic utilities)
- Rename auth.ts → whatsapp-auth.ts (more specific)
- Rename scheduler.ts → task-scheduler.ts (more specific)
- Update all references in docs and imports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 17:35:03 +02:00
gavrielc
f25e0f9a10 Remove redundant comments throughout codebase
Keep only comments that explain non-obvious behavior or add context
not apparent from reading the code.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 16:00:44 +02:00
gavrielc
732c624e6b Fix security issues: IPC auth, message logging, container logs
- Add authorization checks to IPC task operations (pause/resume/cancel)
  to prevent cross-group task manipulation
- Only store message content for registered groups; unregistered chats
  only get metadata stored for group discovery
- Container logs now only include full input/output in debug mode;
  default logging omits sensitive message content

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:51:53 +02:00
gavrielc
552b26cc95 Add PreCompact hook for conversation archiving, remove /clear command
- Add PreCompact hook in agent-runner that archives conversations before
  compaction, using session summary from sessions-index.json for filename
- Remove /clear command (programmatic compaction not supported by SDK)
- Add /add-clear to RFS for future implementation
- Update CLAUDE.md templates with memory system instructions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:37:13 +02:00
Gavriel
aa6dcf39d7 Add typing indicator while agent is processing
Shows "typing..." in WhatsApp while the agent container is running.
Uses Baileys sendPresenceUpdate with 'composing' and 'paused' states.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:10:42 +02:00
Gavriel
8ca4c95517 Fix session persistence and auto-start container system
- Fix session mount path: ~/.claude/ now mounts to /home/node/.claude/
  (container runs as 'node' user with HOME=/home/node, not root)
- Fix ~/.gmail-mcp/ mount path similarly
- Use absolute paths for GROUPS_DIR and DATA_DIR (required for container mounts)
- Auto-start Apple Container system on NanoClaw startup
- Update debug skill with session troubleshooting guide
- Update spec.md with startup sequence and troubleshooting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 11:31:52 +02:00
gavrielc
09c0e8142e Add containerized agent execution with Apple Container
- Agents run in isolated Linux VMs via Apple Container
- All groups get Bash access (safe - sandboxed in container)
- Browser automation via agent-browser + Chromium
- Per-group configurable additional directory mounts
- File-based IPC for messages and scheduled tasks
- Container image with Node.js 22, Chromium, agent-browser

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 22:55:57 +02:00
gavrielc
fa13b14dae Add built-in scheduler with group-scoped tasks
- Custom nanoclaw MCP server with scheduling tools (schedule_task,
  list_tasks, get_task, update_task, pause/resume/cancel_task, send_message)
- Tasks run as full agents in their group's context
- Support for cron, interval, and one-time schedules
- Task run logging with duration and results
- Main channel has Bash access for admin tasks (query DB, manage groups)
- Other groups restricted to file operations only
- Updated docs and requirements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 21:00:37 +02:00
gavrielc
0c08e8a034 Use date + time format in message timestamps
Format: [Jan 31 2:35 PM] instead of [14:35:00]
Date provides important context for ongoing conversations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:33:11 +02:00
gavrielc
22bd3d7c58 Store and display sender's WhatsApp name
Use pushName from baileys to get the sender's display name instead
of just the phone number. Falls back to phone number if no name.

Includes migration to add sender_name column to existing databases.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:32:18 +02:00
gavrielc
cbe33f4ba6 Keep trigger in prompt, simplify message formatting
Include the full message with @trigger so agent sees exactly
what was written. Current message is already in missedMessages
query so no need to add separately.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:30:32 +02:00
gavrielc
f2afb11e71 Include missed messages when catching up the agent
When a triggered message comes in, fetch all messages in that chat
since the last agent interaction and include them in the prompt.
Each message is formatted with timestamp and sender.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:29:09 +02:00
gavrielc
4ec74e9120 Simplify runAgent: just pass the prompt
- Remove context building (group name, chat JID, permissions)
- Agent runs in group folder which determines its permissions
- Caller handles response prefixing and where to send reply
- Session provides continuity, no need for metadata in each message

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:24:55 +02:00
gavrielc
545cbc7b9c Remove unnecessary shutdown handlers
Daemon runs forever; launchd manages lifecycle. SQLite handles
ungraceful shutdowns fine.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:21:59 +02:00
gavrielc
0691601469 Extract database operations into separate db.ts module
- src/db.ts: initDatabase, closeDatabase, storeMessage, getNewMessages
- Removes SQL from index.ts
- Database initialization happens once at startup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:20:41 +02:00
gavrielc
78426c764d Extract config and types into separate files, clean up index.ts
- src/config.ts: configuration constants
- src/types.ts: TypeScript interfaces
- src/index.ts: remove section comments, streamline code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:17:40 +02:00
gavrielc
e1867f8d27 Replace QR code display with macOS notification
The daemon should never show interactive QR codes. If auth is needed,
send a macOS notification alerting the user to run /setup, then exit.

Also removes unused qrcode-terminal dependency.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:01:41 +02:00
gavrielc
295c3e620d Fix: exit gracefully when auth needed in daemon mode
If WhatsApp authentication is required but the process is running
non-interactively (as a launchd daemon), exit with an error instead
of showing a QR code that nobody will see.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 18:57:15 +02:00
gavrielc
c17823a732 Initial commit: NanoClaw - Personal Claude assistant via WhatsApp
A minimal Node.js application that connects Claude Agent SDK to WhatsApp
using baileys. Features per-group memory via CLAUDE.md files, session
continuity, scheduled tasks, and Gmail integration via MCP.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 18:54:24 +02:00