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:
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user