Diagnose why notifications grouped, edited, redirected, or fell through to a new message by replaying recorded JSONL traces from notificationConsumer.
-
Confirm a trace file exists for the window in question. Ask the user for the approximate date/time of the bad grouping behavior, then check:
ls -lah .logs/notif-trace-*.jsonl
If no file exists for the relevant date, stop — there is nothing to replay. Suggest enabling tracing going forward (the consumer writes traces automatically when notifTrace.js is wired; verify by reading server/queue/notificationConsumer.js for trace( calls). Verify file exists before proceeding.
-
Run the broadest sensible query first to see the shape of the data. Use --mode timeline with the narrowest known anchor (a dedup_key, commit SHA, room, or activityId):
node scripts/replay-notif.js --mode timeline --dedup 'github:commit:abc1234'
node scripts/replay-notif.js --mode timeline --commit abc1234de56
node scripts/replay-notif.js --mode timeline --room int-dev-private --since '2026-05-15T14:00' --until '2026-05-15T15:00'
node scripts/replay-notif.js --mode timeline --activity 1715787600123
--mode timeline prints events in chronological order with ts, kind, dedup_key, room, activityId. Verify the events for the reported incident appear before proceeding. If nothing prints, broaden filters (drop --since/--until, try --event push or --event check_run).
-
Identify the decision kind that explains the bug. Read the kind column for each event and map it to behavior:
kind | Meaning | Look for |
|---|
recent_lookup | Consumer queried Redis for a recent activityId on this dedup_key | hit:true (edit path) vs hit:false (new message path) |
recent_saved | Consumer stored a new activityId against the dedup_key for future edits | Confirms a baseline message was sent |
edit_skipped_no_convref | Edit path aborted because no convref:{conversationId} was cached | Bot never observed inbound activity in that room — fix by triggering any inbound message or running syncConversationReferences() |
edit_fell_through | Edit attempt failed at continueConversation time; consumer sent a new message instead | Inspect err field — auth/transient/rateLimit classification from server/lib/retry.js |
announce_redirect | Message was redirected to int-dev-announce by filters.js (low-signal GitHub event) | Confirms expected filtering; the user may want the filter disabled or the event upgraded |
batch_merge | Multiple envelopes merged into one coalesced send within the same tick | count field shows how many; check NOTIF_COALESCE_MAX_ITEMS/MAX_CHARS if unexpected |
batch_skipped | A candidate envelope was excluded from a batch (e.g. different room, dedup conflict) | Cross-check the skipped item's dedup_key against the batch's |
Verify which kind explains the user's report before proposing a fix. If the user reports "commits not grouping" and the timeline shows recent_lookup hit:false followed by recent_saved for the same dedup_key, the edit window expired — check NOTIF_EDIT_WINDOW_MS (default 1800000) and NOTIF_COMMIT_GROUP_WINDOW_MS (default 180000).
-
Switch to --mode grouped to reconstruct the activity-level story. Once you know the offending dedup_key or activityId, run:
node scripts/replay-notif.js --mode grouped --dedup 'github:commit:abc1234'
node scripts/replay-notif.js --mode grouped --activity 1715787600123
This collapses per-tick events under each activityId so you can see the lifecycle: initial send → N edits → expiry. Verify the lifecycle matches what the user saw in Teams. A common mismatch: trace shows successful edits but Teams shows N separate messages — the user is looking at a different dedup_key (e.g. a workflow_job for a different commit). Loop back to Step 3 with the correct anchor.
-
Use --mode raw only when timeline/grouped omit a field you need. Raw prints the full JSON object per line — useful for inspecting extra, err.code, classification, or fallback_webhook_url. Pipe to jq for filtering:
node scripts/replay-notif.js --mode raw --dedup 'github:commit:abc1234' | jq 'select(.kind == "edit_fell_through")'
-
Combine filters to narrow noisy days. Filters AND together:
node scripts/replay-notif.js --mode timeline \
--room int-dev-private \
--event workflow_job \
--kind edit_fell_through \
--since '2026-05-15T00:00' --until '2026-05-15T23:59'
--tick N isolates a single consumer tick (one poll cycle) — useful when batch_merge is suspect. --event matches the originating GitHub event type stored in the envelope's extra.event.
-
Cross-reference findings with code. After identifying the kind, open the matching code path in server/queue/notificationConsumer.js (search for the literal kind string in trace( calls) and explain to the user why the consumer made that decision. Quote file path + line numbers in your reply so the user can jump straight there.
-
Report findings to the user as a short timeline. Format:
14:02:11 — push event github:commit:abc1234 → recent_lookup hit:false → recent_saved activity=...
14:02:14 — workflow_job → recent_lookup hit:true → edit OK
14:05:33 — workflow_job → recent_lookup hit:false (window expired) → new message
Then state the root cause in one sentence and link to the line in notificationConsumer.js that produced the final kind.
Result: user gets a clear answer with the line of code that decided the behavior, and a concrete next step.
Result: root cause identified by clock math against an env var, with the exact knob to turn.