---
name: "Stage transition notifications can arrive out of order and show wrong story name"
agent: coder-opus
---

# Bug 462: Stage transition notifications can arrive out of order and show wrong story name

## Description

When a story moves through stages quickly (e.g. QA → Merge → Done), the stage transition notifications can arrive out of order in Matrix chat. The Done notification appears before the Merge notification.

Two issues:

1. **Out-of-order delivery**: When two notifications are sent close together, the Matrix homeserver can deliver them in the wrong order. The notification handler processes events sequentially and awaits each send, but the homeserver does not guarantee ordering for near-simultaneous messages.

2. **Missing story name on stale notifications**: The second notification shows the raw item_id instead of the story name because `read_story_name` looks in the stage directory from the event (e.g. `4_merge/`), but the file has already moved to the next stage (e.g. `5_done/`).

3. **`inferred_from_stage` guesses the source stage** instead of tracking the actual from-stage. This means skipped stages would show incorrect transitions.

## How to Reproduce

1. Have a story in QA that passes quickly
2. Story moves QA → Merge → Done in rapid succession
3. Observe notifications in Matrix

## Actual Result

Notifications arrive in wrong order (Done before Merge). The later notification shows the raw item_id instead of the story name:

🎉 #32 upgrade TypeScript... — Merge → Done
#32 32_story_upgrade_typescript_and_tsconfig... — QA → Merge

## Expected Result

Notifications arrive in chronological order. All notifications show the story name. Ideally, rapid transitions are coalesced into a single notification for the final stage.

## Acceptance Criteria

- [ ] read_story_name falls back to searching all stages when the expected stage directory has no match
- [ ] Consider deduplicating rapid transitions within a short window (e.g. only notify for the final stage)
- [ ] Track actual from-stage in WatcherEvent instead of guessing via inferred_from_stage
