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
|
## Service Stack Architecture
|
||||||
|
|
||||||
Single unified service manages all three Discord bots in one process:
|
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)
|
- 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)
|
- Paired review: owner (`OWNER_AGENT_TYPE`, default: codex) ↔ reviewer (`REVIEWER_AGENT_TYPE`, default: claude-code)
|
||||||
- Reviewer fallback: Claude 429/한도초과 시 codex-review로 자동 핸드오프
|
- 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.
|
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.
|
Prompt design inspired by [Q00/ouroboros](https://github.com/Q00/ouroboros), adapted for EJClaw's Discord and dual-service workflow.
|
||||||
|
|
||||||
## Overview
|
## 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-main** (`@codex`) — Owner agent. Handles user requests, writes code.
|
||||||
- **Codex** (`@codex`) — OpenAI Codex SDK (`codex exec`), GPT-5.4, xhigh reasoning
|
- **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
|
## Features
|
||||||
|
|
||||||
- **Dual-agent architecture** — Claude Code + Codex as parallel services, shared SQLite (WAL mode)
|
- **Paired review** — Autonomous owner/reviewer ping-pong with verdict-based control
|
||||||
- **Browser automation** — [gstack browse](https://github.com/garrytan/gstack) skill, headless Chromium daemon, ~100ms/command
|
- **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
|
- **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
|
- **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.)
|
- **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
|
- **Browser automation** — [gstack browse](https://github.com/garrytan/gstack) skill, headless Chromium daemon
|
||||||
- **Priority queue** — per-group serialization, global concurrency limit
|
- **Priority queue** — Per-group serialization, global concurrency limit
|
||||||
- **Session persistence** — resume conversations across restarts
|
- **Session persistence** — Resume conversations across restarts
|
||||||
- **Scheduled tasks** — cron/interval/once via MCP tool
|
- **Scheduled tasks** — Cron/interval/once via MCP tool
|
||||||
- **Mid-turn steering** — inject follow-up messages while agent is working (both agents)
|
- **Mid-turn steering** — Inject follow-up messages while agent is working
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Claude Code (Agent SDK, MessageStream)
|
Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Owner (Codex/Claude Code)
|
||||||
└──► Codex (Codex SDK, codex exec)
|
│ │
|
||||||
|
│ ▼ (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)
|
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)
|
- Node.js 20+ (24 recommended, fnm for version management)
|
||||||
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
|
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
|
||||||
- [Codex CLI](https://github.com/openai/codex) (`npm install -g @openai/codex`)
|
- [Codex CLI](https://github.com/openai/codex) (`npm install -g @openai/codex`)
|
||||||
- Bun 1.0+ (for browser automation)
|
- Discord bot tokens (3: Claude, Codex-main, Codex-review)
|
||||||
- Discord bot token
|
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
@@ -74,51 +97,53 @@ npm run build
|
|||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
```bash
|
All configuration in a single `.env` file:
|
||||||
# .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`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# .env.codex
|
# Discord bots (3 tokens for 3 bots)
|
||||||
DISCORD_BOT_TOKEN= # Separate Discord bot token for Codex
|
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
|
# Agent type configuration
|
||||||
DISCORD_BOT_TOKEN= # Separate Discord bot token for Codex Review
|
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
|
### 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.
|
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+).
|
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
|
```bash
|
||||||
npm run restart:stack
|
systemctl --user restart ejclaw # Restart
|
||||||
systemctl --user enable ejclaw ejclaw-codex ejclaw-review
|
systemctl --user status ejclaw # Check status
|
||||||
systemctl --user start ejclaw ejclaw-codex ejclaw-review
|
systemctl --user enable ejclaw # Enable on boot
|
||||||
|
journalctl --user -u ejclaw -f # Follow logs
|
||||||
# Logs
|
|
||||||
journalctl --user -u ejclaw -f
|
|
||||||
journalctl --user -u ejclaw-codex -f
|
|
||||||
journalctl --user -u ejclaw-review -f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `npm run restart:stack` from an external shell or deploy wrapper, not from inside one of the managed EJClaw services.
|
### Deploy
|
||||||
On existing installs, run `npm run setup -- --step service` once after pulling this slice so the `ejclaw-stack-restart.service` unit is installed.
|
|
||||||
|
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
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -128,10 +128,11 @@ export async function runAgentForGroup(
|
|||||||
const isClaudeCodeAgent = effectiveAgentType === 'claude-code';
|
const isClaudeCodeAgent = effectiveAgentType === 'claude-code';
|
||||||
|
|
||||||
// When the reviewer uses a different agent type than the group default,
|
// When the reviewer uses a different agent type than the group default,
|
||||||
// the stored session belongs to the other agent. Don't reuse it.
|
// use a separate session key so owner and reviewer sessions don't collide.
|
||||||
const sessionId = reviewerAgentTypeOverride
|
const sessionFolder = reviewerAgentTypeOverride
|
||||||
? undefined
|
? `${group.folder}:reviewer`
|
||||||
: sessions[group.folder];
|
: group.folder;
|
||||||
|
const sessionId = sessions[sessionFolder];
|
||||||
const memoryBriefing = sessionId
|
const memoryBriefing = sessionId
|
||||||
? undefined
|
? undefined
|
||||||
: await buildRoomMemoryBriefing({
|
: await buildRoomMemoryBriefing({
|
||||||
@@ -307,7 +308,7 @@ export async function runAgentForGroup(
|
|||||||
output.newSessionId &&
|
output.newSessionId &&
|
||||||
!resetSessionRequested
|
!resetSessionRequested
|
||||||
) {
|
) {
|
||||||
deps.persistSession(group.folder, output.newSessionId);
|
deps.persistSession(sessionFolder, output.newSessionId);
|
||||||
}
|
}
|
||||||
const evaluation = evaluateStreamedOutput(output, streamedState, {
|
const evaluation = evaluateStreamedOutput(output, streamedState, {
|
||||||
agentType: isClaudeCodeAgent ? 'claude-code' : 'codex',
|
agentType: isClaudeCodeAgent ? 'claude-code' : 'codex',
|
||||||
@@ -443,7 +444,7 @@ export async function runAgentForGroup(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (provider === 'claude' && output.newSessionId) {
|
if (provider === 'claude' && output.newSessionId) {
|
||||||
deps.persistSession(group.folder, output.newSessionId);
|
deps.persistSession(sessionFolder, output.newSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -657,7 +658,7 @@ export async function runAgentForGroup(
|
|||||||
})));
|
})));
|
||||||
|
|
||||||
if (isRetryableClaudeSessionFailure(primaryAttempt)) {
|
if (isRetryableClaudeSessionFailure(primaryAttempt)) {
|
||||||
deps.clearSession(group.folder);
|
deps.clearSession(sessionFolder);
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ group: group.name, chatJid, runId },
|
{ group: group.name, chatJid, runId },
|
||||||
'Cleared poisoned Claude session before visible output, retrying fresh session',
|
'Cleared poisoned Claude session before visible output, retrying fresh session',
|
||||||
@@ -666,7 +667,7 @@ export async function runAgentForGroup(
|
|||||||
primaryAttempt = await runAttempt('claude');
|
primaryAttempt = await runAttempt('claude');
|
||||||
|
|
||||||
if (isRetryableClaudeSessionFailure(primaryAttempt)) {
|
if (isRetryableClaudeSessionFailure(primaryAttempt)) {
|
||||||
deps.clearSession(group.folder);
|
deps.clearSession(sessionFolder);
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ group: group.name, chatJid, runId },
|
{ group: group.name, chatJid, runId },
|
||||||
'Fresh Claude retry also hit a retryable session failure',
|
'Fresh Claude retry also hit a retryable session failure',
|
||||||
@@ -791,9 +792,9 @@ export async function runAgentForGroup(
|
|||||||
isClaudeCodeAgent &&
|
isClaudeCodeAgent &&
|
||||||
(resetSessionRequested || shouldResetSessionOnAgentFailure(output))
|
(resetSessionRequested || shouldResetSessionOnAgentFailure(output))
|
||||||
) {
|
) {
|
||||||
deps.clearSession(group.folder);
|
deps.clearSession(sessionFolder);
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ group: group.name, chatJid, runId },
|
{ group: group.name, chatJid, runId, sessionFolder },
|
||||||
'Cleared poisoned agent session after unrecoverable error',
|
'Cleared poisoned agent session after unrecoverable error',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user