From ae4996b6a488cf2443e764c2c48f5ce317756fc6 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 22 Aug 2026 03:38:54 -0700 Subject: [PATCH] Chat intake: unconditional tracing from message-persist to turn dispatch (CL-6644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigating a report of a message posting successfully but no agent turn ever being attempted (no error, no fanout log, no reportError, nothing), every log statement on this path today only fires on failure — a turn that silently resolves zero recipients, or one that stalls before dispatchTurnBatch's own try/catch, is indistinguishable from total silence in the logs. Live repro against the shared stack (build daed6b24) confirmed the room's host participant ("myra") was present in settings and the first attempt did reach a sidecar deploy-pack apply, but a repeat send to the same (by then already-deployed) agent produced zero log output at any layer, ruling out the wake/deploy-hang theory #312-#314 already covered as the sole explanation for every occurrence. Root cause not nailed down within this timebox — no fix is guessed here. - routeToRecipients now logs the resolved recipient list (or its emptiness) for every message, not only on failure. - dispatchTurnBatch logs before starting its per-recipient dispatch. - Hub boot logs an explicit confirmation that turnQueue, chatOrchestrator, and chatPlatform were constructed, so a future composition-root wiring mistake shows up in the boot log instead of only in a missing reply days later. Together these turn the exact gap this investigation hit — routing ran successfully but nothing anywhere said so — into a line every boot carries, for whoever picks this back up. --- apps/hub/src/index.ts | 19 ++++++++++++++++ packages/chat/src/workbench-service.ts | 30 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) 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.