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
This commit is contained in:
Eyejoker
2026-03-29 20:50:32 +09:00
parent 8a4d83e2c1
commit 16d20fe627
19 changed files with 325 additions and 642 deletions

View File

@@ -9,9 +9,6 @@ import {
POLL_INTERVAL,
SERVICE_ID,
SERVICE_AGENT_TYPE,
UNIFIED_MODE,
isClaudeService,
isReviewService,
isSessionCommandSenderAllowed,
STATUS_CHANNEL_ID,
STATUS_UPDATE_INTERVAL,
@@ -79,7 +76,6 @@ import {
initTokenRotation,
} from './token-rotation.js';
import {
shouldStartTokenRefreshLoop,
startTokenRefreshLoop,
stopTokenRefreshLoop,
} from './token-refresh.js';
@@ -189,16 +185,11 @@ function loadState(): void {
lastAgentTimestamp = {};
}
sessions = getAllSessions();
// In unified mode, load ALL groups (duplicates resolved by preferring codex).
// In legacy per-service mode, load only this service's registrations.
registeredGroups = UNIFIED_MODE
? getAllRegisteredGroups()
: getAllRegisteredGroups(SERVICE_AGENT_TYPE);
registeredGroups = getAllRegisteredGroups();
logger.info(
{
groupCount: Object.keys(registeredGroups).length,
agentType: SERVICE_AGENT_TYPE,
unifiedMode: UNIFIED_MODE,
},
'State loaded',
);
@@ -331,27 +322,16 @@ async function main(): Promise<void> {
initTokenRotation();
initCodexTokenRotation();
// Unified mode: start credential proxy for container isolation and clean up orphaned containers
if (UNIFIED_MODE) {
startCredentialProxy(CREDENTIAL_PROXY_PORT).catch((err) =>
logger.warn(
{ err },
'Failed to start credential proxy (may already be running)',
),
);
cleanupOrphans();
}
// Start credential proxy for container isolation and clean up orphaned containers
startCredentialProxy(CREDENTIAL_PROXY_PORT).catch((err) =>
logger.warn(
{ err },
'Failed to start credential proxy (may already be running)',
),
);
cleanupOrphans();
// In unified mode, always start the Claude OAuth refresh loop.
// In per-service mode, only the Claude service owns refresh to avoid races.
if (UNIFIED_MODE || shouldStartTokenRefreshLoop(SERVICE_AGENT_TYPE)) {
startTokenRefreshLoop();
} else {
logger.info(
{ serviceAgentType: SERVICE_AGENT_TYPE },
'Skipping Claude OAuth token auto-refresh for non-Claude service',
);
}
startTokenRefreshLoop();
loadState();
@@ -456,27 +436,19 @@ async function main(): Promise<void> {
}
// Start subsystems (independently of connection handler)
// In unified mode always run the scheduler. In per-service mode, skip for review-only service.
if (UNIFIED_MODE || !isReviewService()) {
startSchedulerLoop({
registeredGroups: () => registeredGroups,
getSessions: () => sessions,
queue,
onProcess: (groupJid, proc, processName, ipcDir) =>
queue.registerProcess(groupJid, proc, processName, ipcDir),
sendMessage: (jid, rawText) =>
sendFormattedChannelMessage(channels, jid, rawText),
sendTrackedMessage: (jid, rawText) =>
sendFormattedTrackedChannelMessage(channels, jid, rawText),
editTrackedMessage: (jid, messageId, rawText) =>
editFormattedTrackedChannelMessage(channels, jid, messageId, rawText),
});
} else {
logger.info(
{ serviceId: SERVICE_ID },
'Skipping scheduler for review service',
);
}
startSchedulerLoop({
registeredGroups: () => registeredGroups,
getSessions: () => sessions,
queue,
onProcess: (groupJid, proc, processName, ipcDir) =>
queue.registerProcess(groupJid, proc, processName, ipcDir),
sendMessage: (jid, rawText) =>
sendFormattedChannelMessage(channels, jid, rawText),
sendTrackedMessage: (jid, rawText) =>
sendFormattedTrackedChannelMessage(channels, jid, rawText),
editTrackedMessage: (jid, messageId, rawText) =>
editFormattedTrackedChannelMessage(channels, jid, messageId, rawText),
});
startIpcWatcher({
sendMessage: (jid, text) => {
const channel = findChannel(channels, jid);
@@ -538,8 +510,7 @@ async function main(): Promise<void> {
purgeOnStart: true,
});
if (UNIFIED_MODE || isClaudeService()) {
leaseRecoveryTimer = setInterval(() => {
leaseRecoveryTimer = setInterval(() => {
if (!hasAvailableClaudeToken()) {
return;
}
@@ -578,7 +549,6 @@ async function main(): Promise<void> {
);
}
}, 5_000);
}
runtime.startMessageLoop().catch((err) => {
logger.fatal({ err }, 'Message loop crashed unexpectedly');
process.exit(1);