In paired rooms, bot messages now show sender as "오너" or "리뷰어"
instead of the Discord nickname. Uses channel.isOwnMessage() to
identify which bot sent each message, then maps via the channel
lease to determine the role. This ensures agents correctly identify
who is the owner and who is the reviewer regardless of bot nicknames.
When reviewer was triggered via the normal message dispatch path (not
the review_ready check path), output was sent through the owner channel.
Now checks paired task status to determine the correct output channel.
- 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
- 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
Two leak paths caused Discord typing indicators to persist indefinitely:
1. If runAgent() threw before turnController.finish(), setTyping(false)
was never called. Wrap the agent run in try/finally to guarantee cleanup.
2. Follow-up message piping called setTyping(true) fire-and-forget. If
the Promise resolved after finish()'s setTyping(false), the typing
interval was recreated with nobody to clear it. Await the call to
serialize the on/off sequence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two v1/v2 regressions fixed:
1. Progress messages became too brittle: editMessage failure killed all
subsequent progress updates for the turn. Now resets progressMessageId
and recreates on next update instead of permanently stopping.
2. Follow-up messages stuck in active agent without output: after 10s of
no output, closeStdin and requeue the follow-up as a fresh run with
cursor rollback, instead of silently dropping it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove duplicate processGroupMessages, runAgent, startMessageLoop, and
recoverPendingMessages from index.ts. All message processing now flows
through createMessageRuntime() in message-runtime.ts, eliminating the
dual-path issue that caused the v1 follow-up turn reset bug.
index.ts reduced from ~2300 to ~1210 lines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>