Review: a blip swallows no escalation or waiting reminder; replace safely (issue 373)
mesh/merge-gate pass: builds desk-channel, messenger, telegram → novox; no bus step; every machine composes with the change as it did without (4 of 4 compo…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery-group group fix/373-one-message-per-condition ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request

- A comeback before the clearing was said rings when it is urgent now, and
  says the reminder or escalation that was waiting when it cleared.
- Comebacks never said are counted apart, so "again, Nth time" counts only
  what the operator was told; an urgent comeback after a said clearing rings
  again in place of its message.
- telegram sends the new message before deleting the earlier one, so a
  failed send leaves the operator a message.
- desk-channel keeps a machine's notification on record when its replace
  fails, and says a message shown nowhere as failed.
- A store that cannot be read sends a new message instead of holding it.
- Tests for each, and for a reminder in a burst and a clearing the channel
  could not take.
This commit is contained in:
jochen
2026-10-10 14:22:53 +02:00
parent 3345ff9317
commit f8515219b1
12 changed files with 229 additions and 35 deletions
+3 -2
View File
@@ -9,8 +9,9 @@ as the operator.
buttons — each button carries only the router's one-time ticket — or an edit of a message shown before.
It sends to the account the router names as linked, or, before one is, to the chat a `/start` came from.
A message sent under a handle already shown (a reminder, an escalation) **replaces** it (issue 373): the
earlier message is deleted and the new one sent, ringing as its kind rings, since an edit here notifies
nobody; one the service will not delete loses its buttons and says a newer message follows.
new message is sent, ringing as its kind rings (an edit here notifies nobody), and then the earlier one is
deleted; one the service will not delete loses its buttons and says a newer message follows. A send that
fails leaves the earlier message.
- **Says what the operator chose** on `intake` under its own kind: a tapped button (`choice`), with the
sender as Telegram authenticated it; `/start` (`link`); a message it could not deliver (`failed`); and
whether it can send (`standing`). Only a private chat with the account itself counts.
+17 -11
View File
@@ -11,7 +11,8 @@ package main
// clearing arrives without sound, an urgent message or a question rings (D4). A rate limit is waited out
// as told (D5); a refusal is a permanent failure, said to the router, never retried; an edit to the same
// text is a success (D6). A message that cannot be delivered after three tries is said as failed. A
// `send` or `show` under a handle already shown replaces that message: deleted, and the new one sent.
// `send` or `show` under a handle already shown replaces that message: the new one sent, then the earlier
// one deleted.
// - **What arrives** is long-polled: outbound only. A tapped button is said as a `choice` with the sender
// the service authenticated, and acknowledged on the phone with the router's word on it — what was
// recorded, or why it was not — which comes as a reply to the tap; without one in TapWait, with words that
@@ -232,19 +233,21 @@ func (c *Channel) Take(subject string, body json.RawMessage) error {
}
words, buttons := text(m), c.buttons(m)
silent := m.Silent || (!m.Urgent && len(buttons) == 0)
var place *Placed
if verb == asks.Edit || !m.Secret {
// **A message said again under a handle already shown replaces it** (issue 373): a reminder or an
// escalation is one message per problem, not one more beside it. An edit here notifies nobody, so this one
// is sent, ringing as its kind rings, and only then is the earlier one deleted — one the service will not
// delete loses its buttons and says a newer message follows. A send that fails leaves the earlier one.
var place, earlier *Placed
switch {
case verb == asks.Edit:
if place, err = c.Store.Placed(m.Handle); err != nil {
return err // the state is away: offered again
}
}
if verb != asks.Edit && place != nil {
// **A message said again under a handle already shown replaces it** (issue 373): a reminder or an
// escalation is one message per problem, not one more beside it. An edit here notifies nobody, so the
// earlier message is deleted and this one sent, ringing as its kind rings; one the service will not
// delete loses its buttons and says where the newer one is.
c.replaced(*place)
place = nil
case !m.Secret:
if earlier, err = c.Store.Placed(m.Handle); err != nil {
c.Logf("[telegram] where an earlier message under this handle is could not be read (%v): sent as a new one", err)
earlier = nil
}
}
for try := 1; ; try++ {
var id int64
@@ -261,6 +264,9 @@ func (c *Channel) Take(subject string, body json.RawMessage) error {
err = c.newMessage(m.Handle, chat, words, buttons, silent, &id)
}
if err == nil {
if earlier != nil {
c.replaced(*earlier)
}
c.mu.Lock()
c.lastSent, c.lastErr = c.Now(), ""
c.delivered++
@@ -625,3 +625,18 @@ func TestAMessageSaidAgainUnderItsHandleReplacesTheEarlierOne(t *testing.T) {
t.Errorf("its buttons stayed: %s", kb)
}
}
// The review of PR 176: the new message is sent before the earlier one is deleted, so a send that fails leaves
// the operator the earlier message rather than none.
func TestAReplacementThatCannotBeSentLeavesTheEarlierMessage(t *testing.T) {
c, f, _, st := newChannel(t, goodToken)
st.Place("m1", Placed{Chat: 42, ID: 7})
f.answer["sendMessage"] = func(map[string]any) (int, string) {
return 400, `{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}`
}
subject, body := work(asks.Send, asks.Message{Handle: "m1", Title: "Still open: ace silent", Urgent: true, To: "42"})
_ = c.Take(subject, body)
if len(f.of("deleteMessage")) != 0 || st.placed["m1"].ID != 7 {
t.Fatalf("the earlier message was taken away though its replacement was not sent: %+v", f.of("deleteMessage"))
}
}