Release v0.106.0 — duplicates judged by what the room heard; no stall on an empty list - #105
Merged
Merged
Conversation
… on an empty list
An identical announcement was answered "duplicate" only from the web UI's own
memory of having sent one. That was wrong both ways: the same words reached a
room twice when the first copy was a scheduler replay, went out unjudged, or
played before a restart; and "duplicate" was answered after the room had since
been asked for something different, or when the first copy never played.
- delivery.content_verdict(): sorts every earlier ask to a room into HEARD /
IN FLIGHT / WAITING / GONE and answers ("heard", row), ("pending", row) or
None. A retirement is not "gone" — the retry may retire a clip 20 s after an
attempt still playing. "Pending" only counts while a replay is possible.
- app.py: the ledger governs when it can answer; the in-memory window decides
when it cannot (unreadable, no rows for that room, no key, window off).
Rows carry a keyed content tag (HMAC, key in /data), never a plain hash of
predictable speech in the shared folder.
- Neither duplicate row is an ask: a repeat never displaces the copy the room
is already owed, and never retires anything. The scheduler's retry retires a
waiting replay as content-delivered.
- ami.actions_complete(): a list action also finishes when its FIRST reply is
an error — how Asterisk answers an empty contact or endpoint list — so a
poll with no phone registered no longer waits out its timeout. A mid-list
error still waits for the Complete. Both single list reads are tagged now;
stream_complete is retired; every login sends Events: off.
- A trunk refusal is no longer read as "no trunk configured", a refused
registration kick is no longer reported as sent, and the LAN /announce
exemption is narrowed to the speaker clips that need it.
951 tests; 35-mutant battery, no survivors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the two items left open after v0.105.2.
1. The same announcement could play twice — or never
"Duplicate" was answered from one fact the web UI held in memory: that it had itself sent identical audio. Past that, the same words reached a room twice (the first copy was a scheduler replay, went out while the state read could not judge, or played before a restart); inside it, "duplicate" was answered after the room had since been asked for something different, or when the first copy never played.
delivery.content_verdict()now sorts every earlier ask to that room into heard / in flight / waiting / gone and answers("heard", row),("pending", row)orNone. A retirement is deliberately not "gone": the retry may retire a clip 20 s after an attempt whose call is still playing. The web UI uses the ledger whenever it can answer and the in-memory window only when it cannot (unreadable, no rows for that room, no key, window off). Neither duplicate row is an ask: a repeat never displaces the copy the room is already owed and never retires anything.Rows name content by a keyed tag (HMAC, key in
/data), never a plain hash of predictable speech in the shared folder, and the handset clips are no longer served to the LAN.2. No registered phone meant a 2.5 s poll
Asterisk answers an empty contact list with
Response: Errorand nothing more, so the read waited for aCompletethat never came — every dashboard, console and link-monitor poll took its full timeout exactly when a whole-house outage made it most useful. An error that is an action's first reply now ends it; a mid-list error still waits for theComplete. Both single list reads are tagged,stream_completeis retired, and every login sendsEvents: off. Also: a trunk refusal is no longer read as "no trunk configured", a refused registration kick is no longer reported as sent.Verification
Events: off(status bundle 4.8 ms, endpoints 1.3 ms, registrations 0.5 ms), and a real error-first reply ends in 0.3 ms instead of waiting 2.5 s.🤖 Generated with Claude Code