@@ -257,6 +257,68 @@ function isRetriableWatchSubscribeStartupError(error: unknown): boolean {
257257 ) ;
258258}
259259
260+ const IMESSAGE_DIAGNOSTIC_DROP_REASONS = new Set ( [
261+ "agent echo in self-chat" ,
262+ "echo" ,
263+ "from me" ,
264+ "reflected assistant content" ,
265+ "self-chat echo" ,
266+ ] ) ;
267+ const IMESSAGE_THROTTLED_DIAGNOSTIC_DROP_REASONS = new Set ( [ "from me" ] ) ;
268+
269+ export function shouldThrottleIMessageInboundDropDiagnostic ( reason : string ) : boolean {
270+ return IMESSAGE_THROTTLED_DIAGNOSTIC_DROP_REASONS . has ( reason ) ;
271+ }
272+
273+ export function describeIMessageInboundDropDiagnostic ( params : {
274+ accountId : string ;
275+ reason : string ;
276+ message : Pick < IMessagePayload , "chat_id" | "created_at" | "guid" | "id" | "is_group" > ;
277+ } ) : string | null {
278+ if ( ! IMESSAGE_DIAGNOSTIC_DROP_REASONS . has ( params . reason ) ) {
279+ return null ;
280+ }
281+ const messageId =
282+ typeof params . message . id === "number" || typeof params . message . id === "string"
283+ ? String ( params . message . id )
284+ : "unknown" ;
285+ return (
286+ `imessage: dropped inbound message account=${ params . accountId } reason=${ JSON . stringify (
287+ params . reason ,
288+ ) } ` +
289+ `chat_id=${ params . message . chat_id ?? "unknown" } group=${ params . message . is_group === true } ` +
290+ `message_id=${ messageId } guid=${ params . message . guid ? "present" : "missing" } ` +
291+ `created_at=${ params . message . created_at ?? "unknown" } `
292+ ) ;
293+ }
294+
295+ function describeIMessageWatchSubscribeStartupFailure ( params : {
296+ accountId : string ;
297+ attempt : number ;
298+ maxAttempts : number ;
299+ cliPath : string ;
300+ dbPath ?: string ;
301+ remoteHost ?: string ;
302+ includeAttachments : boolean ;
303+ probeTimeoutMs : number ;
304+ watchSinceRowid : number | null ;
305+ error : unknown ;
306+ retryDelayMs ?: number ;
307+ } ) : string {
308+ const retry = params . retryDelayMs !== undefined ? ` retry_in_ms=${ params . retryDelayMs } ` : "" ;
309+ return (
310+ `imessage: watch.subscribe startup failed attempt=${ params . attempt } /${ params . maxAttempts } ` +
311+ `account=${ params . accountId } cliPath=${ params . cliPath } ` +
312+ `dbPath=${ params . dbPath ? "configured" : "default" } remoteHost=${
313+ params . remoteHost ? "configured" : "none"
314+ } ` +
315+ `timeoutMs=${ params . probeTimeoutMs } since_rowid=${ params . watchSinceRowid ?? "none" } ` +
316+ `attachments=${ params . includeAttachments } include_reactions=true${ retry } : ${ String (
317+ params . error ,
318+ ) } `
319+ ) ;
320+ }
321+
260322async function waitForWatchSubscribeRetryDelay ( params : {
261323 ms : number ;
262324 abortSignal ?: AbortSignal ;
@@ -363,6 +425,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
363425 // replay aggressively without the old catchup cursor/retry bookkeeping.
364426 const inboundReplayGuard = createIMessageInboundReplayGuard ( ) ;
365427 let staleBacklogSuppressed = 0 ;
428+ const loggedThrottledDropDiagnostics = new Set < string > ( ) ;
366429
367430 // Downtime recovery. We pass the persisted recovery cursor (the last
368431 // dispatched rowid) to watch.subscribe as since_rowid so imsg replays the rows
@@ -849,6 +912,23 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
849912 if ( isLoopDrop ) {
850913 loopRateLimiter . record ( rateLimitKey ) ;
851914 }
915+ const diagnostic = describeIMessageInboundDropDiagnostic ( {
916+ accountId : accountInfo . accountId ,
917+ reason : decision . reason ,
918+ message,
919+ } ) ;
920+ if ( diagnostic ) {
921+ const throttleKey = `${ rateLimitKey } :${ decision . reason } ` ;
922+ const shouldThrottleDiagnostic = shouldThrottleIMessageInboundDropDiagnostic (
923+ decision . reason ,
924+ ) ;
925+ if ( ! shouldThrottleDiagnostic || ! loggedThrottledDropDiagnostics . has ( throttleKey ) ) {
926+ if ( shouldThrottleDiagnostic ) {
927+ loggedThrottledDropDiagnostics . add ( throttleKey ) ;
928+ }
929+ runtime . log ?.( warn ( diagnostic ) ) ;
930+ }
931+ }
852932 // Surface the silent-allowlist drop once per chat. Without this, operators
853933 // who set groupPolicy="allowlist" without populating
854934 // channels.imessage.groups see every group message vanish at default log
@@ -1422,12 +1502,39 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
14221502 const shouldRetry =
14231503 attempt < WATCH_SUBSCRIBE_MAX_ATTEMPTS && isRetriableWatchSubscribeStartupError ( err ) ;
14241504 if ( ! shouldRetry ) {
1425- runtime . error ?.( danger ( `imessage: monitor failed: ${ String ( err ) } ` ) ) ;
1505+ runtime . error ?.(
1506+ danger (
1507+ `imessage: monitor failed: ${ describeIMessageWatchSubscribeStartupFailure ( {
1508+ accountId : accountInfo . accountId ,
1509+ attempt,
1510+ maxAttempts : WATCH_SUBSCRIBE_MAX_ATTEMPTS ,
1511+ cliPath,
1512+ dbPath,
1513+ remoteHost,
1514+ includeAttachments,
1515+ probeTimeoutMs,
1516+ watchSinceRowid,
1517+ error : err ,
1518+ } ) } `,
1519+ ) ,
1520+ ) ;
14261521 throw err ;
14271522 }
14281523 runtime . log ?.(
14291524 warn (
1430- `imessage: watch.subscribe startup failed (attempt ${ attempt } /${ WATCH_SUBSCRIBE_MAX_ATTEMPTS } ): ${ String ( err ) } ; retrying` ,
1525+ describeIMessageWatchSubscribeStartupFailure ( {
1526+ accountId : accountInfo . accountId ,
1527+ attempt,
1528+ maxAttempts : WATCH_SUBSCRIBE_MAX_ATTEMPTS ,
1529+ cliPath,
1530+ dbPath,
1531+ remoteHost,
1532+ includeAttachments,
1533+ probeTimeoutMs,
1534+ watchSinceRowid,
1535+ error : err ,
1536+ retryDelayMs : WATCH_SUBSCRIBE_RETRY_DELAY_MS ,
1537+ } ) ,
14311538 ) ,
14321539 ) ;
14331540 // Tear down the failed client before waiting so a slow subscribe attempt
0 commit comments