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:
78
src/index.ts
78
src/index.ts
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user