Commit Graph

49 Commits

Author SHA1 Message Date
Eyejoker
f4b04d6c4d feat: add Mixture of Agents (MoA) for arbiter verdicts
When MOA_ENABLED=true, arbiter queries multiple LLM models in parallel
via OpenAI-compatible API, then an aggregator model synthesizes the
final verdict from all opinions. Falls back to single-agent arbiter
when MoA is disabled.

Config: MOA_BASE_URL, MOA_API_KEY, MOA_REFERENCE_MODELS, MOA_AGGREGATOR_MODEL
2026-03-31 00:20:41 +09:00
Eyejoker
3e2954ebcf refactor: extract resolveEffectiveAgentType/resolveSessionFolder as SSOT, deduplicate CONTAINER_IMAGE 2026-03-30 23:44:49 +09:00
Eyejoker
e649432a40 feat: add per-role model selection via .env (OWNER/REVIEWER/ARBITER_MODEL, _EFFORT, _FALLBACK_ENABLED) 2026-03-30 23:40:36 +09:00
Eyejoker
5562e30ce1 refactor: use resolveActiveRole() as SSOT for all status→role mapping
Eliminated duplicate task status checks in message-agent-executor.ts
and message-runtime.ts. All status→role decisions now flow through
the single resolveActiveRole() function.
2026-03-30 23:03:00 +09:00
Eyejoker
a043f2b79a feat: implement MAGI 3-agent arbiter system for deadlock resolution
Introduces a third agent role (arbiter) that is summoned on-demand
when owner and reviewer reach a deadlock (same verdict 3+ rounds).

Architecture:
- 3 Discord bots: owner (codex), reviewer (claude), arbiter (claude/codex)
- Arbiter is NOT always-on — only invoked when deadlock detected
- Arbiter renders binding verdict: PROCEED/REVISE/RESET/ESCALATE
- Non-escalate verdicts reset round_trip_count and resume ping-pong
- Backward compatible: ARBITER_AGENT_TYPE unset = existing 2-agent mode

Changes across 13 source files + 7 test files:
- types.ts: PairedRoomRole += 'arbiter', new statuses, ArbiterVerdict type
- config.ts: ARBITER_AGENT_TYPE, ARBITER_SERVICE_ID, ARBITER_DEADLOCK_THRESHOLD
- db.ts: schema migration (arbiter columns in channel_owner + paired_tasks)
- service-routing.ts: arbiter_service_id in lease
- room-role-context.ts: arbiter role detection
- paired-execution-context.ts: deadlock->arbiter, verdict handling
- message-runtime.ts: arbiter turn routing, cursor, sender labeling
- message-agent-executor.ts: arbiter mode, failover
- agent-runner.ts + environment.ts: arbiter container mode
- platform-prompts.ts: arbiter prompt loading

New files:
- prompts/arbiter-paired-room.md: arbiter system prompt
- src/arbiter-context.ts: builds conversation context for arbiter judgment
2026-03-30 22:20:42 +09:00
Eyejoker
78fa078921 fix: only reset merge_ready to active when human message is present
The finalize turn (auto-triggered after reviewer approval) was being
treated as a new working turn because merge_ready was unconditionally
reset to active. Now merge_ready is only reset when a human message
is in the batch, preserving the finalize flow for bot-only turns.
2026-03-30 19:48:25 +09:00
Eyejoker
e51afc27f0 feat: failover reviewer to codex when Claude reviewer is unavailable
When the reviewer is Claude and hits auth/429/overloaded errors,
hand off the review turn to codex-review instead of swapping
owner/reviewer roles (which is the owner failover path).
2026-03-30 18:15:17 +09:00
Eyejoker
5221fb8b33 style: fix prettier drift after merge 2026-03-30 03:44:58 +09:00
Eyejoker
e863db61a8 refactor: remove dead output suppression path 2026-03-30 03:22:08 +09:00
Eyejoker
8049db0d47 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.
2026-03-29 22:22:45 +09:00
Eyejoker
b33cf6c058 debug: forward runner query lifecycle logs to INFO for diagnosis 2026-03-29 21:29:06 +09:00
Eyejoker
8119b0bb8f fix: treat execution as success when output was delivered despite null result 2026-03-29 21:24:49 +09:00
Eyejoker
16d20fe627 feat: paired review system overhaul — unified service, configurable agents, silent output removal
- Remove UNIFIED_MODE legacy: single service manages all 3 Discord bots
- Add OWNER_AGENT_TYPE / REVIEWER_AGENT_TYPE env vars for configurable agent selection
- Fix Discord channel routing: reviewer output goes through correct bot (discord/discord-review)
- Fix channel name assignment: explicit names prevent agentTypeFilter collision
- Remove silent output suppression system: harness-level protections replace prompt workarounds
- Add reviewer approval detection: DONE marker on first line stops ping-pong
- Include user's original message in reviewer prompt for context
- Fix round_trip_count auto-reset on new user message
- Fix session ID conflict: reviewer doesn't reuse owner's session
- Fix pending progress text flush in runner on close sentinel
- Promote buffered intermediate text to final result when result event has no text
- Remove legacy files: .env.codex.example, .env.codex-review.example, migrate-unify.cjs
- Update CLAUDE.md: server-side build deployment, unified architecture docs
2026-03-29 20:50:32 +09:00
Eyejoker
3e607d7344 fix: enqueue next turn after paired execution so reviewer runs without new message 2026-03-29 19:08:07 +09:00
Eyejoker
4dc4b3bc01 fix: route paired room turns by task status instead of SERVICE_ID
In unified mode, SERVICE_SESSION_SCOPE doesn't match the lease's
owner/reviewer service IDs. Determine the effective role from the
paired task status: review_ready/in_review → reviewer, otherwise → owner.
This restores the owner↔reviewer ping-pong in unified single-process mode.
2026-03-29 19:04:51 +09:00
Eyejoker
a1e5418a2f feat: auto-trigger reviewer after owner completion with round trip limit
- Owner finishes → auto mark review_ready → reviewer executes
- Reviewer finishes → task back to active → owner can respond
- Automatic ping-pong until consensus or round trip limit
- PAIRED_MAX_ROUND_TRIPS env var (default 10, 0 = unlimited)
- round_trip_count tracked per task in DB
2026-03-29 18:57:16 +09:00
Eyejoker
d01f98bd61 feat: unify 3 bot services into single process
- Add UNIFIED_MODE flag (default on, disable with UNIFIED_MODE=0)
- Register all 3 Discord bots in one process (claude, codex, review)
- Load all registered groups regardless of agent_type (codex/owner priority)
- Start credential proxy and container cleanup at unified startup
- shouldServiceProcessChat returns true in unified mode
- Add findChannelByName for role-based response routing
- Backward compatible: UNIFIED_MODE=0 restores per-service behavior
2026-03-29 18:37:17 +09:00
Eyejoker
fc9f2867b9 refactor: simplify paired review system and add reviewer container isolation
Phase 1 — Strip over-engineering (-3,665 lines):
- DB tables: 7 → 3 (remove executions, approvals, artifacts, events)
- Task statuses: 11 → 5 (active, review_ready, in_review, merge_ready, completed)
- Remove plan governance, event sourcing, gate/verdict, freshness guards
- paired-execution-context: 980 → 299 lines
- Session commands: remove /risk, /plan, /approve-plan, /request-plan-changes

Phase 2 — Container isolation for reviewers:
- Add container-runtime.ts (Docker abstraction)
- Add credential-proxy.ts (API key injection without container exposure)
- Add container-runner.ts (reviewer-specific read-only mount + tmpfs)
- Add container/Dockerfile + agent-runner (Chromium, Claude Code, Codex)
- agent-runner.ts: auto-route reviewer execution to container mode
2026-03-29 18:24:56 +09:00
Eyejoker
dad6f50937 feat: enforce paired reviewer gate verdicts 2026-03-29 06:03:45 +09:00
Eyejoker
06b6326a9d style: sync paired workspace formatting fallout 2026-03-28 21:15:16 +09:00
Eyejoker
e7f49d77da feat: add paired review workspace flow 2026-03-28 21:14:25 +09:00
Eyejoker
79a8a2639e feat: add structured room role metadata 2026-03-28 19:30:32 +09:00
Eyejoker
fe6314108c feat: add structured silent output contract 2026-03-28 05:56:58 +09:00
Eyejoker
e1fdc47552 style: sync pre-commit formatting fallout 2026-03-28 05:41:16 +09:00
Eyejoker
ba9f6871b6 feat: add codex review failover and suppress output hardening 2026-03-28 05:40:38 +09:00
Eyejoker
d1c693fb17 fix: treat signal-killed agents as success when output was delivered
Two bugs fixed:

1. agent-runner: process close handler only checked `code !== 0`,
   but signal kills (SIGTERM/SIGKILL from post-close cleanup) set
   code=null which was misclassified as error even after successful
   output delivery. Now checks `code === null && signal` and resolves
   as success when hadStreamingOutput is true.

2. message-agent-executor: wrappedOnOutput persisted newSessionId
   before checking for poisoned session, allowing a stale session to
   be re-saved after clearSession. Reordered to check poison first
   and guard persist with !resetSessionRequested.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 21:25:30 +09:00
Eyejoker
6806001ed6 style: fix prettier formatting after type union refactor
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 17:46:54 +09:00
Eyejoker
94eeaa5e3e refactor: introduce AgentTriggerReason type union as SSOT for error reason strings
Replace scattered reason string literals with a centralized type hierarchy:
- AgentTriggerReason: all possible trigger reasons
- ClaudeRotationReason, CodexRotationReason, NoFallbackCooldownReason:
  derived subtypes for specific contexts
- AgentErrorClassification, FallbackTriggerResult, CodexRotationTriggerResult:
  discriminated unions for compile-time narrowing

Remove dead export isUsageExhausted() (superseded by
isPrimaryNoFallbackCooldownActive). Replace NO_FALLBACK_COOLDOWN_REASONS
Set with isNoFallbackCooldownReason() type guard.

Add tests for agent-error-detection and provider-retry (423 total passing).

Known limitation: 6 `as CodexRotationReason` casts in Codex rotation paths
where streamed trigger reason is AgentTriggerReason but runtime value is
always CodexRotationReason-compatible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 17:46:43 +09:00
Eyejoker
60a9fe86ec fix: detect org-access-denied errors, suppress chat output, and rotate Claude tokens
When a Claude account is suspended, the API returns "Your organization does
not have access to Claude" on the success path or "Failed to authenticate.
API Error: 403 terminated" on the error path. Both are now classified as
'org-access-denied', suppressed from Discord output, and trigger automatic
token rotation. If all tokens are exhausted, enters cooldown without Kimi
fallback (same as usage-exhausted and auth-expired).

Changes:
- agent-error-detection: add isClaudeOrgAccessDeniedMessage(), expand
  classifyClaudeAuthError() and shouldRotateClaudeToken()
- streamed-output-evaluator: detect org-access-denied in success-path chain
- provider-fallback: add NO_FALLBACK_COOLDOWN_REASONS set and
  isPrimaryNoFallbackCooldownActive() helper
- provider-retry: handle org-access-denied in rotation loop
- message-agent-executor / task-scheduler: use generalized no-fallback
  cooldown check
- Tests: +8 test cases across 5 test files (415 total passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 17:24:10 +09:00
Eyejoker
273d52d3da fix: unify agentType value domain to 'claude-code' | 'codex'
The streamed-output-evaluator used 'claude' while the rest of the
codebase used 'claude-code'. Align the evaluator type and all call
sites (message-agent-executor, task-scheduler) to use the canonical
'claude-code' | 'codex' domain consistently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 15:51:09 +09:00
Eyejoker
eebee00e60 refactor: share streamed output evaluation 2026-03-25 11:46:16 +09:00
Eyejoker
5ea3439c5f refactor: extract shared utilities, protocol constants, and retry loop
Phase 3: provider-retry.ts — shared Claude rotation loop (SSOT)
  - retryClaudeWithRotation extracted from message-agent-executor + task-scheduler
  - ~200 lines of duplicated retry logic removed

Phase 4: types.ts — AgentOutputPhase + VisiblePhase unified
Phase 5: types.ts — AgentConfig extended with claudeThinking/claudeThinkingBudget

Utilities (utils.ts):
  - getErrorMessage: 14 occurrences of instanceof Error pattern → 1 function
  - readJsonFile/writeJsonFile: 19 occurrences of JSON+fs pattern → 2 functions
  - fetchWithTimeout: 3 occurrences of AbortController pattern → 1 function
  - formatElapsedKorean: deduplicated from task-watch-status + message-turn-controller

Protocol (agent-protocol.ts):
  - OUTPUT_START/END_MARKER centralized (runners keep local copies with SSOT reference)
  - IMAGE_TAG_RE, IPC constants documented

Runner: show "대화 요약 중..." progress message during auto-compact

Net: -137 lines, 354/354 tests passing
2026-03-25 04:59:49 +09:00
Eyejoker
906a3dfadb config: add auto-compact settings for Claude and Codex agents
Claude: CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=50 (~500K on 1M context)
Codex: model_auto_compact_token_limit=258000 (matching CLI default)
2026-03-25 03:56:03 +09:00
Eyejoker
744e2ce6e2 style: format agent rotation updates 2026-03-24 17:09:17 +09:00
Eyejoker
8b2c46b89e fix: harden agent shutdown and auth rotation 2026-03-24 17:08:48 +09:00
Eyejoker
6fc1c77418 fix: suppress 401 auth errors from chat output
401 authentication errors are now filtered from Discord messages
and only logged. Prevents "Failed to authenticate" from reaching users.
2026-03-24 15:42:29 +09:00
Eyejoker
991f2bf2fd test: align fallback expectations after merge 2026-03-24 03:56:07 +09:00
Eyejoker
0724914a43 merge: integrate remote token rotation + memory pipeline
Merge remote main (token rotation, usage API, dashboard fixes)
with local memory pipeline changes. Smart quote encoding fixed.
Test alignment pending.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 03:53:15 +09:00
Eyejoker
3467e245d5 feat: auto memory pipeline — host-driven Memento recall/reflect
Add automatic memory integration so EJClaw host directly calls Memento
MCP for recall at session start and reflect on compact, removing
reliance on agent voluntary tool use.

Stage 1: New sessions get room memory briefing injected into system
prompt (CLAUDE.md / AGENTS.md) via host-side MCP client.

Stage 2: PreCompact hook automatically calls reflect + remember to
persist session summaries as room-memory fragments.

Also restores missing source files (token-rotation, codex-token-rotation,
claude-usage exports) to fix full build from source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 03:48:36 +09:00
Eyejoker
6a73421d4f fix: use ignoreRateLimits in retryClaudeWithRotation loops
Without this, token rotation was blocked by stale cooldowns even
when the target token had available usage.
2026-03-24 03:47:22 +09:00
Eyejoker
120f16e9e1 fix: Claude token rotation, usage-exhausted fallback, and usage API caching
- Fix token rotation not taking effect: getCurrentToken() was shadowed
  by static .env CLAUDE_CODE_OAUTH_TOKEN value in agent environment
- Don't fall back to Kimi on usage-exhausted (only on transient 429/network)
- Add disk cache for Claude usage API data (survives restarts, 429s)
- Rate-limit usage API calls to 1 per token per 5 minutes
- Add ignoreRateLimits option to rotateToken() for exhausted recovery
- Rotate to next token in getActiveProvider() when current is exhausted
- Add isUsageExhausted() helper to provider-fallback
2026-03-24 03:40:51 +09:00
Eyejoker
c51d80bd07 style: prettier formatting 2026-03-24 01:37:14 +09:00
Eyejoker
60dab43a3f feat: parse retry-after from errors, cache codex usage per account
- Parse "try again at" time from rate-limit errors instead of
  fixed 1-hour cooldown, with 3-min buffer after reset
- Pass error message to rotateToken/rotateCodexToken for parsing
- Cache usage % and reset time per Codex account for dashboard
- Persist cached usage in rotation state file across restarts
- Show rate-limited Codex accounts with cached 100% + reset time
- Fix dashboard text indicators (*!/space) for monospace alignment
2026-03-24 01:16:54 +09:00
Eyejoker
30be180f4b fix: codex rate-limit retry immediately instead of next request 2026-03-23 23:07:35 +09:00
Eyejoker
816b0a39cc feat: Codex account rotation + fix rate-limit detection
- New codex-token-rotation module: rotates between multiple
  ~/.codex-accounts/{n}/auth.json on rate-limit
- Copies active account auth to session dir before each spawn
- Fix detectFallbackTrigger to match "usage limit" / "hit your limit"
- Fix streamed error detection: remove claude-only guard so codex
  rate-limit errors are caught even when output.status is "success"
- Add rotation in both task-scheduler and message-agent-executor
2026-03-23 23:06:17 +09:00
Eyejoker
d378598f2e feat: Claude OAuth token rotation on rate-limit
- New token-rotation module: stores multiple tokens from
  CLAUDE_CODE_OAUTH_TOKENS env var (comma-separated)
- On rate-limit, rotates to next healthy token before falling
  back to Kimi provider
- Also fixes: intermediate text routes to progress message
  when no subagents active (prevents message flooding)
2026-03-23 22:01:06 +09:00
Eyejoker
77e79505eb feat: auto-suspend tasks on quota errors, add Claude progress tracking
- Auto-suspend tasks after 3 consecutive quota/auth errors with
  retry-after date parsing and Discord notification
- Add tool_progress and tool_use_summary streaming from Claude Agent SDK
- Prefix progress messages with invisible marker so bots ignore them
- Fix misleading provider label in codex service logs
2026-03-23 17:03:17 +09:00
Eyejoker
9def49f7fb fix: isolate watcher task runtimes 2026-03-23 07:16:07 +09:00
Eyejoker
898c77fcfc refactor: consolidate runtime helpers 2026-03-23 06:39:26 +09:00