Skip to content

fix: read push survives a locked Mac, and imsg-read says the screen is locked - #133

Merged
nixfred merged 3 commits into
nixfred:mainfrom
ianswope:fix/read-push-locked-mac
Oct 7, 2026
Merged

nixfred merged 3 commits into
nixfred:mainfrom
ianswope:fix/read-push-locked-mac

Conversation

@ianswope

@ianswope ianswope commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

What

Three changes, one cause.

  1. A per-chat read intent is never dropped. collect() used to delete pendingReads[chat] after five failed attempts. Now only --all is dropped on failure (it cannot be bounded); a per-chat intent keeps retrying at the existing backoff ceiling (once a minute) until the Mac confirms it or a newer inbound cancels it. Two new tests: a prolonged failure keeps the dot suppressed through seven attempts and recovery clears the queue with one more push; a newer inbound mid-retry cancels the intent, leaves the chat unread and sends nothing. The read-sync fixture also sets XDG_RUNTIME_DIR into its home: without that, a running blip-bridged answers underneath the tests on any machine that runs Blip, and the convergence test fails on clean main there. That test's budget goes from 20 s to 60 s; it spawns 96 collector processes and takes 10–16 s on an idle 16-core box.

  2. imsg-read names the lock screen. When System Events fails or finds no Messages window, it now reads IOConsoleUsers[].CGSSessionScreenIsLocked from ioreg -n Root -d1 -a (no TCC grant involved; the key exists only while locked) and reports "the Mac's screen is locked …" with the cure, instead of "log in and open Messages on the Mac". The Accessibility-not-granted message keeps precedence.

  3. Docs. README "Keeping the Mac awake" gets the lock screen as the third way a gateway goes dark, with the one-line fix, its trade-off and undo, and a Linux-side check. CLAUDE.md records the durable per-chat queue and the lock-screen finding.

Why

My gateway Mac mini never sleeps (sleep 0, displaysleep 0), never logs out, and Messages is always open. Under push_read=thread, every --chat push still failed with exit 64 "Messages has no accessible window" from about 25 minutes after I last touched it until I unlocked it again, and I had been "logging in" several times a day without understanding why. The mini's loginwindow log has the answer: screen saver at 20 idle minutes (targetUserIdle = 1200, the default), lock 5 minutes later (sysadminctl -screenLock status), and behind the lock screen count windows of process "Messages" is 0. The last failed push was at 10:26:05 today; LWScreenLock handleUnlockResult logged the unlock at 10:26:07; Blip was healthy two minutes later.

Two things made it worse than it had to be. The error text sent me to a Messages that was open and fine. And the collector dropped each per-chat intent after five attempts, so a conversation I had read in Blip came back as unread after the night. The intent was right; the Mac was just unreachable for it. Keeping the intent is safe because the read-state snapshot compare and --through already refuse to consume a newer inbound, and the second new test pins that.

The fix for the lock itself is on the Mac, not in Blip: defaults -currentHost write com.apple.screensaver idleTime -int 0. I applied it today and confirmed from the loginwindow log that when the armed timer fired at 10:46:07 it re-read idleTime: 0 and stood down. Nothing here touches #125's items (the per-poll snapshot, the blocking read action).

How it was verified

  • bun test is green locally (748 pass, 0 fail). python3 bridge/mac/test_read.py 35 OK (three new), test_read_verification.py and test_read_sync.py OK, the CI py_compile line passes.
  • screen_locked() run verbatim on the gateway (macOS 27.0.1, Python 3.9.6) against the live, unlocked session returns False. The locked branch is covered by the synthetic-plist test; I have not locked the mini on purpose to see it live.
  • The collector change has run on my machine since yesterday: through last night's lock it kept two per-chat intents (441 and 409 attempts) and both pushed and cleared within minutes of the unlock, with no dot resurrected.
  • Any send/receive behavior was exercised against my own number only: no send/receive behavior touched.
  • UI change → screenshot attached: no visual change.
  • Touches an invariant in CLAUDE.md → "Read sync is an acknowledged, durable action queue" (per-chat intents are never dropped) and the "Marking read ON THE MAC" notes (the lock screen); both updated.

…olates the live channel

collect() deleted pendingReads[chat] after five failed attempts. Only --all
is dropped now (it cannot be bounded); a per-chat intent keeps retrying at
the backoff ceiling (once a minute) until the Mac confirms it or a newer
inbound cancels it. Dropping one resurrected a dot the user had already read
through intent.seen: after a night with the gateway's screen locked, two
intents sat at 441 and 409 attempts and cleared within minutes of the unlock
once they were kept. Keeping them is safe because the snapshot compare and
--through already refuse to consume a newer inbound.

Two tests: a prolonged failure keeps the dot suppressed through seven
attempts and recovery clears the queue with one more push; a newer inbound
mid-retry cancels the intent, leaves the chat unread and sends nothing.

The read-sync fixture now sets XDG_RUNTIME_DIR into its home. Without it a
running blip-bridged answers underneath the tests on any machine that runs
Blip, and the convergence test fails on clean main there. That test's budget
goes from 20 s to 60 s: 96 collector spawns take 10-16 s on an idle 16-core
box and tip over 20 s with anything else running.
Behind the lock screen every app's windows vanish from System Events, so a
logged-in, awake Mac with Messages open fails every --chat push with
"Messages has no accessible window; log in and open Messages on the Mac",
which sends the owner to a Messages that is open and fine. The defaults do
this 25 minutes after the last touch: screen saver at 20 idle minutes, lock
5 minutes later.

screen_locked() reads IOConsoleUsers[].CGSSessionScreenIsLocked from
`ioreg -n Root -d1 -a` (no TCC grant involved; the key exists only while
locked). select_chat() and accessibility() consult it only after System
Events has already failed or found no window, and report the lock with the
cure. The Accessibility-not-granted message keeps precedence. Run verbatim
on a macOS 27.0.1 gateway under its Python 3.9.6 the function returns False
for the live unlocked session; the locked branch and the parse failures are
covered by tests on synthetic plists.
…queue

README: the lock screen joins sleep and the login window as the third way a
gateway goes dark, with why only the read push stops, the one-line fix
(`defaults -currentHost write com.apple.screensaver idleTime -int 0`), its
trade-off and undo, and a Linux-side check that needs no permission prompt.

CLAUDE.md: the read-sync queue bullet says a per-chat intent is never dropped
and why that is safe; the Mac read-push notes record the lock-screen finding
and that imsg-read now names it.
@ianswope
ianswope requested a review from nixfred as a code owner October 6, 2026 15:57
nixfred added a commit that referenced this pull request Oct 7, 2026
WHAT: in split view the displayed chat's pinned tile or row gets a persistent
selected fill (Style.selectedFillFor), following isShowing() by identity, not
hover, focus or position; openThread() stops the pending peek timer and moves
the sidebar cursor to the opened chat without calling cursorMoved(), so a
committed open never arms another preview (onCursorChanged only builds rows:
'a peeked thread is not read' holds). Flyout unchanged. From Brandon Smith
(github.com/bndnsmth, new contributor).
WHY: clicking a chat after the keyboard cursor sat on the first row left the
first row lit, or nothing lit once hover ended, while the right pane showed
another conversation: misleading when replying.
VERIFIED: merged with main, 748 bun pass (with the live socket hidden; see
#133), the 2 new ui tests fail on the old BlipView; door two-way; Fred
approved 2026-10-06.

LR-T: blip, sidebar, selection, split-view, ui, bndnsmth
LR-D: blip
LR-R: #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeRMdPWNC2myz9U8hEeeCi
@nixfred
nixfred merged commit 1cc45ca into nixfred:main Oct 7, 2026
1 check passed
@nixfred

nixfred commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Merged, thank you Ian. A night of 441 failed attempts against a locked Mac, then a dot coming back for a conversation already read, is a great find, and "the Mac's screen is locked" is a far better message than sending someone to a Messages that is open and fine. Your fixture change also turned out to matter here more than you knew: with the persistent channel from #83 running on my machine, 15 read-sync tests on main were failing because the live socket answered underneath them; with your XDG_RUNTIME_DIR isolation all 750 pass. The new bridge is on my Mac and both Linux machines. One note for the record, not a change request: per-chat retries are now unbounded, so a permanent non-lock failure on an unlocked Mac would reopen the conversation once a minute; in practice opening it reads it and the snapshot retires the intent, and only push_read=thread or menu reads queue per-chat intents.

nixfred added a commit that referenced this pull request Oct 7, 2026
…h asked for his X handle)

LR-T: blip, change-log, weekly-notes, roster, #133, #134, bndnsmth, ianswope
LR-D: blip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeRMdPWNC2myz9U8hEeeCi
@ianswope
ianswope deleted the fix/read-push-locked-mac branch October 7, 2026 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants