diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index 40540c07..1c27b2cd 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -1364,6 +1364,25 @@ export async function createHub(config: HubConfig) { chatOrchestratorDeps.memory = memoryHandle.memory; } const chatOrchestrator = createChatOrchestrator(chatOrchestratorDeps); + // CL-6644: a loud, unconditional boot confirmation that message intake + // is actually wired — a composition-root mistake here (an import + // dropped, a construction reordered, an argument omitted) type-checks + // fine but produces a hub that accepts messages into a void: no + // dispatch, no error, no notice, just a message that persists and is + // never asked of anyone. This can't detect every such mistake (the + // pieces below are non-optional local bindings, not feature-flagged), + // but it turns "intake is wired" from an assumption nothing checks + // into a line every boot log carries — the next investigation starts + // by grepping for this instead of re-deriving the whole call chain. + getLogger(["hub", "chat-intake"]).info( + "Chat message intake wired: turnQueue={hasTurnQueue} " + + "chatOrchestrator={hasOrchestrator} chatPlatform={hasPlatform}", + { + hasTurnQueue: turnQueue !== undefined, + hasOrchestrator: chatOrchestrator !== undefined, + hasPlatform: chatPlatform !== undefined, + }, + ); // A room participant that died with its sidecar is otherwise silently // dead until somebody writes into it, and the turn the crash // interrupted never surfaces at all — the run that died never sends diff --git a/packages/chat/src/workbench-service.ts b/packages/chat/src/workbench-service.ts index a26a4205..2644f0bb 100644 --- a/packages/chat/src/workbench-service.ts +++ b/packages/chat/src/workbench-service.ts @@ -1138,6 +1138,25 @@ async function routeToRecipients( if (host !== undefined) recipientSet.add(host.address); } const recipients = [...recipientSet]; + // CL-6644: unconditional, not gated on failure — the silent gap this + // investigation found is that nothing at all logs between "message + // persisted" and either a dispatch failure or a successful reply, + // so a turn that resolves zero recipients (a workbench with no host + // participant, a stale settings row) or one that stalls before ever + // reaching `dispatchTurnBatch`'s own error handling looks identical + // to total silence in the logs. This line exists so the next person + // chasing an "agent never replied" report can tell, from logs alone, + // whether routing ever ran and what it resolved to. + fanoutLog.info( + "Routed workbench {workbenchId}'s message {messageId} to {count} " + + "recipient(s): {recipients}", + { + workbenchId: input.workbenchId, + messageId, + count: recipients.length, + recipients, + }, + ); const contextText = !isDefaultRouting && recipients.length > 0 @@ -1202,6 +1221,17 @@ async function dispatchTurnBatch( if (last === undefined) return; const messageIds = batch.map((turn) => turn.messageId); + // CL-6644: unconditional entry marker — see the matching note on the + // caller's own recipient-resolution log. This is the one line that + // proves execution reached turn dispatch at all; its absence for a + // message known to have persisted narrows a future "no reply" + // report to upstream of here without needing a live repro first. + fanoutLog.info( + "Dispatching workbench {workbenchId}'s turn for message(s) " + + "{messageIds} to {count} recipient(s): {recipients}", + { workbenchId, messageIds, count: recipients.length, recipients }, + ); + // Concurrent: agents are independent, and a dispatch that has to wake // its target pays a full redeploy — serially, one slept agent would // delay every agent mentioned after it.