fix: separate reviewer session from owner to preserve review context
Reviewer was starting with undefined sessionId every turn, losing all conversation context. Now uses `group.folder:reviewer` as the session key so owner and reviewer maintain independent session histories. Also updates README to reflect unified service architecture and paired review workflow.
This commit is contained in:
@@ -58,7 +58,7 @@ ssh clone-ej@100.64.185.108 'cd ~/EJClaw && git pull && npm run build && npm run
|
||||
## Service Stack Architecture
|
||||
|
||||
Single unified service manages all three Discord bots in one process:
|
||||
- `ejclaw.service` — Unified process, `UNIFIED_MODE=true` (default)
|
||||
- `ejclaw.service` — Single unified process
|
||||
- Discord bots: `DISCORD_BOT_TOKEN` (Claude), `DISCORD_CODEX_BOT_TOKEN` (Codex-main), `DISCORD_REVIEW_BOT_TOKEN` (Codex-review)
|
||||
- Paired review: owner (`OWNER_AGENT_TYPE`, default: codex) ↔ reviewer (`REVIEWER_AGENT_TYPE`, default: claude-code)
|
||||
- Reviewer fallback: Claude 429/한도초과 시 codex-review로 자동 핸드오프
|
||||
|
||||
123
README.md
123
README.md
@@ -5,41 +5,65 @@
|
||||

|
||||

|
||||
|
||||
Dual-agent AI assistant (Claude Code + Codex) over Discord.
|
||||
Dual-agent AI assistant (Claude Code + Codex) over Discord with autonomous paired review.
|
||||
|
||||
Originally derived from [qwibitai/nanoclaw](https://github.com/qwibitai/nanoclaw), now maintained as EJClaw for personal production use.
|
||||
Prompt design inspired by [Q00/ouroboros](https://github.com/Q00/ouroboros), adapted for EJClaw's Discord and dual-service workflow.
|
||||
|
||||
## Overview
|
||||
|
||||
Two AI agents running as parallel systemd services on a single host:
|
||||
A single unified service (`ejclaw`) manages three Discord bots in one process:
|
||||
|
||||
- **Claude Code** (`@claude`) — Anthropic Agent SDK, adaptive thinking, Opus/Sonnet
|
||||
- **Codex** (`@codex`) — OpenAI Codex SDK (`codex exec`), GPT-5.4, xhigh reasoning
|
||||
- **Codex-main** (`@codex`) — Owner agent. Handles user requests, writes code.
|
||||
- **Claude** (`@claude`) — Reviewer agent. Critically reviews owner's work, verifies design direction.
|
||||
- **Codex-review** (`@codex-review`) — Fallback reviewer. Takes over when Claude hits rate limits (429).
|
||||
|
||||
Both share the same codebase (`dist/index.js`), differentiated by environment variables. No containers — direct host processes for zero overhead.
|
||||
Owner and reviewer agent types are configurable via `OWNER_AGENT_TYPE` and `REVIEWER_AGENT_TYPE` in `.env`.
|
||||
|
||||
## Paired Review
|
||||
|
||||
The core workflow is an autonomous owner-reviewer ping-pong:
|
||||
|
||||
```
|
||||
User message
|
||||
→ Owner responds (implementation, answer, etc.)
|
||||
→ Reviewer auto-triggered (critical review, design check)
|
||||
→ Verdict:
|
||||
DONE → Owner gets final turn to finalize → Task completed
|
||||
DONE_WITH_CONCERNS → Owner addresses feedback → Reviewer re-reviews
|
||||
BLOCKED → Escalate to user (needs decision)
|
||||
NEEDS_CONTEXT → Escalate to user (missing information)
|
||||
```
|
||||
|
||||
The system stops autonomously when the reviewer approves or escalates. No manual intervention needed for the happy path.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dual-agent architecture** — Claude Code + Codex as parallel services, shared SQLite (WAL mode)
|
||||
- **Browser automation** — [gstack browse](https://github.com/garrytan/gstack) skill, headless Chromium daemon, ~100ms/command
|
||||
- **Paired review** — Autonomous owner/reviewer ping-pong with verdict-based control
|
||||
- **Configurable agent types** — Owner and reviewer roles independently set to `claude-code` or `codex`
|
||||
- **Reviewer fallback** — Claude 429/exhaustion → automatic handoff to codex-review
|
||||
- **Voice transcription** — Groq Whisper (primary) / OpenAI Whisper (fallback), shared file cache with dedup
|
||||
- **Bidirectional images** — receive Discord attachments as multimodal input, send screenshots back
|
||||
- **Bidirectional images** — Receive Discord attachments as multimodal input, send screenshots back
|
||||
- **Token rotation** — Claude 429 / usage exhaustion → automatic multi-account rotation
|
||||
- **CI monitoring** — `watch_ci` MCP tool for GitHub Actions run polling (structured fast path, no LLM token cost)
|
||||
- **Usage dashboard** — real-time token usage and service status overview
|
||||
- **MCP integration** — Memento (persistent memory) + EJClaw host tools (send_message, schedule_task, watch_ci, etc.)
|
||||
- **Skill sync** — single source of truth, auto-synced to all agent sessions
|
||||
- **Priority queue** — per-group serialization, global concurrency limit
|
||||
- **Session persistence** — resume conversations across restarts
|
||||
- **Scheduled tasks** — cron/interval/once via MCP tool
|
||||
- **Mid-turn steering** — inject follow-up messages while agent is working (both agents)
|
||||
- **Browser automation** — [gstack browse](https://github.com/garrytan/gstack) skill, headless Chromium daemon
|
||||
- **Priority queue** — Per-group serialization, global concurrency limit
|
||||
- **Session persistence** — Resume conversations across restarts
|
||||
- **Scheduled tasks** — Cron/interval/once via MCP tool
|
||||
- **Mid-turn steering** — Inject follow-up messages while agent is working
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Claude Code (Agent SDK, MessageStream)
|
||||
└──► Codex (Codex SDK, codex exec)
|
||||
Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Owner (Codex/Claude Code)
|
||||
│ │
|
||||
│ ▼ (auto-trigger)
|
||||
└──► Reviewer (Claude Code/Codex)
|
||||
│ │
|
||||
│ Verdict routing
|
||||
│ ├─ DONE → merge_ready → Owner finalizes
|
||||
│ ├─ BLOCKED/NEEDS_CONTEXT → User
|
||||
│ └─ Feedback → Owner (loop)
|
||||
│
|
||||
IPC polling ◄── follow-up messages (mid-turn steering)
|
||||
│
|
||||
@@ -59,8 +83,7 @@ Each agent has access to:
|
||||
- Node.js 20+ (24 recommended, fnm for version management)
|
||||
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
|
||||
- [Codex CLI](https://github.com/openai/codex) (`npm install -g @openai/codex`)
|
||||
- Bun 1.0+ (for browser automation)
|
||||
- Discord bot token
|
||||
- Discord bot tokens (3: Claude, Codex-main, Codex-review)
|
||||
|
||||
### Install
|
||||
|
||||
@@ -74,51 +97,53 @@ npm run build
|
||||
|
||||
### Environment
|
||||
|
||||
```bash
|
||||
# .env
|
||||
DISCORD_BOT_TOKEN= # Discord bot token
|
||||
ASSISTANT_NAME=claude # Bot trigger name (@claude)
|
||||
CLAUDE_CODE_OAUTH_TOKEN= # Claude Code OAuth token (primary)
|
||||
CLAUDE_CODE_OAUTH_TOKENS= # Comma-separated tokens for multi-account rotation
|
||||
OPENAI_API_KEY= # For Codex
|
||||
GROQ_API_KEY= # Voice transcription (Groq Whisper)
|
||||
```
|
||||
|
||||
### Codex Service (optional)
|
||||
|
||||
To run the Codex agent alongside Claude, create `.env.codex`:
|
||||
All configuration in a single `.env` file:
|
||||
|
||||
```bash
|
||||
# .env.codex
|
||||
DISCORD_BOT_TOKEN= # Separate Discord bot token for Codex
|
||||
# Discord bots (3 tokens for 3 bots)
|
||||
DISCORD_BOT_TOKEN= # Claude bot
|
||||
DISCORD_CODEX_BOT_TOKEN= # Codex-main bot (owner)
|
||||
DISCORD_REVIEW_BOT_TOKEN= # Codex-review bot (fallback reviewer)
|
||||
|
||||
# .env.codex-review
|
||||
DISCORD_BOT_TOKEN= # Separate Discord bot token for Codex Review
|
||||
# Agent type configuration
|
||||
OWNER_AGENT_TYPE=codex # codex | claude-code
|
||||
REVIEWER_AGENT_TYPE=claude-code # claude-code | codex
|
||||
|
||||
# API keys
|
||||
CLAUDE_CODE_OAUTH_TOKEN= # Claude Code OAuth token
|
||||
CLAUDE_CODE_OAUTH_TOKENS= # Comma-separated tokens for multi-account rotation
|
||||
OPENAI_API_KEY= # For Codex
|
||||
GROQ_API_KEY= # Voice transcription (Groq Whisper)
|
||||
|
||||
# Bot names
|
||||
ASSISTANT_NAME=claude # Claude bot trigger name
|
||||
CODEX_ASSISTANT_NAME=codex # Codex bot trigger name
|
||||
```
|
||||
|
||||
The setup step (`npm run setup -- --step service`) auto-detects `.env.codex` and `.env.codex-review`, then installs `ejclaw-codex` and `ejclaw-review` alongside `ejclaw`. Additional Codex settings (`CODEX_MODEL`, `CODEX_EFFORT`, etc.) can be added to `.env.codex`, `.env.codex-review`, or as `Environment=` lines in the systemd units.
|
||||
|
||||
### Authentication
|
||||
|
||||
Multi-account OAuth token rotation is supported via `CLAUDE_CODE_OAUTH_TOKENS` (comma-separated). When one account hits a rate limit, the system automatically rotates to the next.
|
||||
|
||||
Token auto-refresh runs on the Claude service only, refreshing access tokens 30 minutes before expiry using rotating refresh tokens from `~/.claude/.credentials.json` (account 0) and `~/.claude-accounts/{n}/.credentials.json` (account 1+). Generate tokens with `claude setup-token` (account 0) or `CLAUDE_CONFIG_DIR=~/.claude-accounts/{n} claude setup-token` (account 1+).
|
||||
|
||||
### Systemd Services (Linux)
|
||||
### Systemd Service (Linux)
|
||||
|
||||
Single unified service:
|
||||
|
||||
```bash
|
||||
npm run restart:stack
|
||||
systemctl --user enable ejclaw ejclaw-codex ejclaw-review
|
||||
systemctl --user start ejclaw ejclaw-codex ejclaw-review
|
||||
|
||||
# Logs
|
||||
journalctl --user -u ejclaw -f
|
||||
journalctl --user -u ejclaw-codex -f
|
||||
journalctl --user -u ejclaw-review -f
|
||||
systemctl --user restart ejclaw # Restart
|
||||
systemctl --user status ejclaw # Check status
|
||||
systemctl --user enable ejclaw # Enable on boot
|
||||
journalctl --user -u ejclaw -f # Follow logs
|
||||
```
|
||||
|
||||
Use `npm run restart:stack` from an external shell or deploy wrapper, not from inside one of the managed EJClaw services.
|
||||
On existing installs, run `npm run setup -- --step service` once after pulling this slice so the `ejclaw-stack-restart.service` unit is installed.
|
||||
### Deploy
|
||||
|
||||
Build on server, not locally:
|
||||
|
||||
```bash
|
||||
ssh clone-ej@100.64.185.108 'cd ~/EJClaw && git pull && npm run build && npm run build:runners && systemctl --user restart ejclaw'
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -128,10 +128,11 @@ export async function runAgentForGroup(
|
||||
const isClaudeCodeAgent = effectiveAgentType === 'claude-code';
|
||||
|
||||
// When the reviewer uses a different agent type than the group default,
|
||||
// the stored session belongs to the other agent. Don't reuse it.
|
||||
const sessionId = reviewerAgentTypeOverride
|
||||
? undefined
|
||||
: sessions[group.folder];
|
||||
// use a separate session key so owner and reviewer sessions don't collide.
|
||||
const sessionFolder = reviewerAgentTypeOverride
|
||||
? `${group.folder}:reviewer`
|
||||
: group.folder;
|
||||
const sessionId = sessions[sessionFolder];
|
||||
const memoryBriefing = sessionId
|
||||
? undefined
|
||||
: await buildRoomMemoryBriefing({
|
||||
@@ -307,7 +308,7 @@ export async function runAgentForGroup(
|
||||
output.newSessionId &&
|
||||
!resetSessionRequested
|
||||
) {
|
||||
deps.persistSession(group.folder, output.newSessionId);
|
||||
deps.persistSession(sessionFolder, output.newSessionId);
|
||||
}
|
||||
const evaluation = evaluateStreamedOutput(output, streamedState, {
|
||||
agentType: isClaudeCodeAgent ? 'claude-code' : 'codex',
|
||||
@@ -443,7 +444,7 @@ export async function runAgentForGroup(
|
||||
);
|
||||
|
||||
if (provider === 'claude' && output.newSessionId) {
|
||||
deps.persistSession(group.folder, output.newSessionId);
|
||||
deps.persistSession(sessionFolder, output.newSessionId);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -657,7 +658,7 @@ export async function runAgentForGroup(
|
||||
})));
|
||||
|
||||
if (isRetryableClaudeSessionFailure(primaryAttempt)) {
|
||||
deps.clearSession(group.folder);
|
||||
deps.clearSession(sessionFolder);
|
||||
logger.warn(
|
||||
{ group: group.name, chatJid, runId },
|
||||
'Cleared poisoned Claude session before visible output, retrying fresh session',
|
||||
@@ -666,7 +667,7 @@ export async function runAgentForGroup(
|
||||
primaryAttempt = await runAttempt('claude');
|
||||
|
||||
if (isRetryableClaudeSessionFailure(primaryAttempt)) {
|
||||
deps.clearSession(group.folder);
|
||||
deps.clearSession(sessionFolder);
|
||||
logger.warn(
|
||||
{ group: group.name, chatJid, runId },
|
||||
'Fresh Claude retry also hit a retryable session failure',
|
||||
@@ -791,9 +792,9 @@ export async function runAgentForGroup(
|
||||
isClaudeCodeAgent &&
|
||||
(resetSessionRequested || shouldResetSessionOnAgentFailure(output))
|
||||
) {
|
||||
deps.clearSession(group.folder);
|
||||
deps.clearSession(sessionFolder);
|
||||
logger.warn(
|
||||
{ group: group.name, chatJid, runId },
|
||||
{ group: group.name, chatJid, runId, sessionFolder },
|
||||
'Cleared poisoned agent session after unrecoverable error',
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user