merge: integrate origin/main (406 commits) into live branch

Merge upstream's paired-room stabilization, dedicated message-runtime-loop,
outbound delivery refactor (ipc-outbound-delivery / deliverFormattedCanonicalMessage),
discord module split, dashboard rework, and migrations 015-018 into our branch.

Conflict policy: prefer upstream for the heavily-refactored paired-room /
message-runtime / db cluster (it supersedes our earlier loop band-aids), keep
only orthogonal unique fixes:
- codex bundled-JS launcher fix (codex-warmup.ts)
- slot-aligned Claude+Codex usage primer (usage-primer.ts) re-wired into index.ts
- MemoryHigh=3G cgroup bound, discord perms diagnostic, prompt docs
- progress null-race + silent-failure publish guards (message-turn-controller.ts)

Dropped (superseded by upstream or unwired): reviewer STEP_DONE/TASK_DONE loop
band-aids, router markdown-escape override, register tribunal-default+git-init,
restart-context rewindToSeq (unwired).

Verified: typecheck clean, build clean, vitest shows zero new regressions vs the
origin/main baseline (only pre-existing env-config failures remain).
This commit is contained in:
Codex
2026-06-08 21:15:39 +09:00
409 changed files with 53523 additions and 10332 deletions

View File

@@ -4,11 +4,12 @@ import {
getAgentOutputText,
} from './agent-output.js';
import { createScopedLogger, logger } from './logger.js';
import { sanitizeForOutbound } from './router.js';
import { formatOutbound } from './router.js';
import { shouldResetSessionOnAgentFailure } from './session-recovery.js';
import { TASK_STATUS_MESSAGE_PREFIX } from './task-watch-status.js';
import { formatElapsedKorean } from './utils.js';
import type { PairedTurnIdentity } from './paired-turn-identity.js';
import { isHumanMessageCloseReason } from './message-close-reasons.js';
import {
normalizeAgentOutputPhase,
toVisiblePhase,
@@ -45,10 +46,12 @@ interface MessageTurnControllerOptions {
},
) => Promise<boolean>;
canDeliverFinalText?: () => boolean;
getCloseReason?: () => string | null;
allowProgressReplayWithoutFinal?: boolean;
deliveryRole?: PairedRoomRole | null;
deliveryServiceId?: string | null;
pairedTurnIdentity?: PairedTurnIdentity | null;
recordTurnProgress?: (turnId: string, progressText: string) => void;
}
export class MessageTurnController {
@@ -166,10 +169,7 @@ export class MessageTurnController {
}
const raw = getAgentOutputText(result);
// Use sanitize (no markdown escape) — the Discord channel boundary
// applies the final escape pass, and double-escaping would produce
// visible backslash garbage.
const text = raw ? sanitizeForOutbound(raw) : null;
const text = raw ? formatOutbound(raw) : null;
const attachments = getAgentOutputAttachments(result);
if (raw) {
@@ -352,11 +352,9 @@ export class MessageTurnController {
visiblePhase: VisiblePhase;
}> {
await this.deactivateTyping('turn:finish', { outputStatus });
if (outputStatus === 'error') {
this.hadError = true;
}
if (
outputStatus === 'success' &&
this.visiblePhase === 'progress' &&
@@ -364,7 +362,9 @@ export class MessageTurnController {
this.latestProgressTextForFinal
) {
const replayText = this.latestProgressTextForFinal;
if (this.options.allowProgressReplayWithoutFinal !== false) {
if (isHumanMessageCloseReason(this.options.getCloseReason?.() ?? null)) {
this.resetProgressState();
} else if (this.options.allowProgressReplayWithoutFinal !== false) {
this.log.info(
'Sending a separate final message from the last progress output after agent completion',
);
@@ -407,6 +407,52 @@ export class MessageTurnController {
return this.visiblePhase === 'final';
}
private composeProgressBody(text: string): string {
if (this.subagents.size > 1) {
const lines: string[] = [];
for (const [, track] of this.subagents) {
const latest = track.activities[track.activities.length - 1];
lines.push(latest ? `${track.label} · ${latest}` : track.label);
}
return lines.join('\n');
}
if (this.subagents.size === 1) {
const [, track] = this.subagents.entries().next().value!;
const lines: string[] = [track.label];
for (let i = 0; i < track.activities.length; i++) {
const isLast = i === track.activities.length - 1;
lines.push(`${isLast ? '└' : '├'} ${track.activities[i]}`);
}
return lines.join('\n');
}
const activityLines =
this.toolActivities.length > 0
? '\n' +
this.toolActivities
.map((a, i) => {
const isLast = i === this.toolActivities.length - 1;
const connector = isLast ? '└' : '├';
const isSummary = a.startsWith('📋');
return isSummary ? `${connector} ${a}` : `${connector} ${a}`;
})
.join('\n')
: '';
return text + activityLines;
}
private persistProgressBody(body: string): void {
const turnId = this.options.pairedTurnIdentity?.turnId;
if (!turnId || !this.options.recordTurnProgress) return;
try {
this.options.recordTurnProgress(turnId, body);
} catch (err) {
this.log.warn(
{ err, turnId, bodyLength: body.length },
'Failed to persist progress body',
);
}
}
private renderProgressMessage(text: string): string {
const elapsedMs =
this.progressStartedAt === null
@@ -414,41 +460,9 @@ export class MessageTurnController {
: Math.floor((Date.now() - this.progressStartedAt) / 5_000) * 5000;
const suffix = `\n\n${formatElapsedKorean(elapsedMs)}`;
let body: string;
const body = this.composeProgressBody(text);
if (this.subagents.size > 1) {
// Compact: one line per subagent with latest activity
const lines: string[] = [];
for (const [, track] of this.subagents) {
const latest = track.activities[track.activities.length - 1];
lines.push(latest ? `${track.label} · ${latest}` : track.label);
}
body = lines.join('\n');
} else if (this.subagents.size === 1) {
// Single subagent: detailed view with activity sub-lines
const [, track] = this.subagents.entries().next().value!;
const lines: string[] = [track.label];
for (let i = 0; i < track.activities.length; i++) {
const isLast = i === track.activities.length - 1;
lines.push(`${isLast ? '└' : '├'} ${track.activities[i]}`);
}
body = lines.join('\n');
} else {
// Single agent rendering
const activityLines =
this.toolActivities.length > 0
? '\n' +
this.toolActivities
.map((a, i) => {
const isLast = i === this.toolActivities.length - 1;
const connector = isLast ? '└' : '├';
const isSummary = a.startsWith('📋');
return isSummary ? `${connector} ${a}` : `${connector} ${a}`;
})
.join('\n')
: '';
body = text + activityLines;
}
this.persistProgressBody(body);
const maxBody = 2000 - TASK_STATUS_MESSAGE_PREFIX.length - suffix.length;
const truncated =
@@ -474,6 +488,14 @@ export class MessageTurnController {
this.progressMessageId = null;
this.progressStartedAt = null;
this.progressEditFailCount = 0;
const turnId = this.options.pairedTurnIdentity?.turnId;
if (turnId && this.options.recordTurnProgress) {
try {
this.options.recordTurnProgress(turnId, '');
} catch {
/* clearing progress is best-effort */
}
}
}
/**
@@ -777,13 +799,11 @@ export class MessageTurnController {
}
await this.publishTerminalText(this.options.failureFinalText);
}
private requestAgentClose(reason: string): void {
if (this.closeRequested) return;
this.closeRequested = true;
this.options.requestClose(reason);
}
private async sendProgressMessage(text: string): Promise<void> {
if (
this.options.canDeliverFinalText &&