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:
@@ -214,73 +214,8 @@ function normalizeStructuredOutput(result: string | null): {
|
||||
if (typeof result !== 'string' || result.length === 0) {
|
||||
return { result };
|
||||
}
|
||||
|
||||
const trimmed = result.trim();
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as {
|
||||
ejclaw?: { visibility?: unknown; text?: unknown; verdict?: unknown };
|
||||
};
|
||||
const envelope = parsed?.ejclaw;
|
||||
if (envelope && typeof envelope === 'object' && !Array.isArray(envelope)) {
|
||||
if (envelope.visibility === 'silent') {
|
||||
if (
|
||||
envelope.verdict !== undefined &&
|
||||
envelope.verdict !== 'silent'
|
||||
) {
|
||||
return {
|
||||
result,
|
||||
output: { visibility: 'public', text: result },
|
||||
};
|
||||
}
|
||||
return {
|
||||
result: null,
|
||||
output: {
|
||||
visibility: 'silent',
|
||||
verdict:
|
||||
envelope.verdict === 'silent' ? ('silent' as const) : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (
|
||||
envelope.visibility === 'public' &&
|
||||
typeof envelope.text === 'string' &&
|
||||
envelope.text.length > 0
|
||||
) {
|
||||
if (
|
||||
envelope.verdict !== undefined &&
|
||||
envelope.verdict !== 'done' &&
|
||||
envelope.verdict !== 'done_with_concerns' &&
|
||||
envelope.verdict !== 'blocked'
|
||||
) {
|
||||
return {
|
||||
result,
|
||||
output: { visibility: 'public', text: result },
|
||||
};
|
||||
}
|
||||
return {
|
||||
result: envelope.text,
|
||||
output: {
|
||||
visibility: 'public',
|
||||
text: envelope.text,
|
||||
verdict:
|
||||
typeof envelope.verdict === 'string'
|
||||
? (envelope.verdict as
|
||||
| 'done'
|
||||
| 'done_with_concerns'
|
||||
| 'blocked')
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fall through to legacy string output
|
||||
}
|
||||
|
||||
return {
|
||||
result,
|
||||
output: { visibility: 'public', text: result },
|
||||
};
|
||||
// All output is public — silent suppression was removed.
|
||||
return { result, output: { visibility: 'public', text: result } };
|
||||
}
|
||||
|
||||
function log(message: string): void {
|
||||
@@ -677,6 +612,19 @@ async function runQuery(
|
||||
if (!ipcPolling) return;
|
||||
if (shouldClose()) {
|
||||
log('Close sentinel detected during query, ending stream');
|
||||
// Flush any buffered text before closing — the for-await loop may not
|
||||
// reach the post-loop flush code after stream.end().
|
||||
if (pendingProgressText && !terminalResultObserved) {
|
||||
log(`Flushing pending text before close (${pendingProgressText.length} chars)`);
|
||||
writeOutput({
|
||||
status: 'success',
|
||||
...normalizeStructuredOutput(pendingProgressText),
|
||||
newSessionId,
|
||||
});
|
||||
pendingProgressText = null;
|
||||
terminalResultObserved = true;
|
||||
resultCount++;
|
||||
}
|
||||
closedDuringQuery = true;
|
||||
stream.end();
|
||||
ipcPolling = false;
|
||||
@@ -916,14 +864,21 @@ async function runQuery(
|
||||
|
||||
if (message.type === 'result') {
|
||||
resultCount++;
|
||||
const textResult = 'result' in message ? (message as { result?: string }).result : null;
|
||||
let textResult = 'result' in message ? (message as { result?: string }).result : null;
|
||||
const isError = message.subtype?.startsWith('error');
|
||||
// Discard pending progress if it matches the final result (prevent duplicate)
|
||||
if (pendingProgressText && textResult && pendingProgressText === textResult) {
|
||||
log(`Discarding pending progress (matches result)`);
|
||||
pendingProgressText = null;
|
||||
} else if (pendingProgressText) {
|
||||
writeOutput({ status: 'success', phase: 'intermediate', result: pendingProgressText, newSessionId });
|
||||
// If the result has no text, promote pending progress to the result
|
||||
// so it gets delivered as the final output instead of being lost.
|
||||
if (!textResult) {
|
||||
log(`Promoting pending progress text to result (${pendingProgressText.length} chars)`);
|
||||
textResult = pendingProgressText;
|
||||
} else {
|
||||
writeOutput({ status: 'success', phase: 'intermediate', result: pendingProgressText, newSessionId });
|
||||
}
|
||||
pendingProgressText = null;
|
||||
}
|
||||
log(`Result #${resultCount}: subtype=${message.subtype}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`);
|
||||
@@ -1011,6 +966,19 @@ async function runQuery(
|
||||
}
|
||||
}
|
||||
|
||||
// Flush any remaining buffered text that was never followed by a result event.
|
||||
// This happens when the agent produces a short response without a formal end_turn.
|
||||
if (pendingProgressText && !terminalResultObserved) {
|
||||
log(`Flushing remaining pending progress text as final output (${pendingProgressText.length} chars)`);
|
||||
writeOutput({
|
||||
status: 'success',
|
||||
...normalizeStructuredOutput(pendingProgressText),
|
||||
newSessionId,
|
||||
});
|
||||
terminalResultObserved = true;
|
||||
resultCount++;
|
||||
}
|
||||
|
||||
ipcPolling = false;
|
||||
log(
|
||||
`Query done. Messages: ${messageCount}, results: ${resultCount}, closedDuringQuery: ${closedDuringQuery}`,
|
||||
|
||||
Reference in New Issue
Block a user