Skip to content

Enhance macOS energy efficiency and improve extension reconnect logic - #20

Closed
le-dawg wants to merge 16 commits into
vitalio-sh:mainfrom
le-dawg:main
Closed

le-dawg wants to merge 16 commits into
vitalio-sh:mainfrom
le-dawg:main

Conversation

@le-dawg

@le-dawg le-dawg commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR improves runtime resilience and macOS (on Apple Silicon) energy consumption across the extension and bridge.

The main changes:

  • reduce reconnect behaviour after sleep/wake or idle transitions
  • add bounded shared IPC concurrency in the extension to avoid flooding Thunderbird API

What changed

Extension

  • replace fixed reconnect timing with exponential backoff plus jitter
  • conditional resume monitoring so reconnect recovery works after longer sleep/wake gaps without keeping an always-on timer active
  • reconnect immediately on user return via browser.idle
  • add a shared IPC queue with bounded concurrency for Thunderbird API calls
  • parallelize bulk/message operations through a shared limiter:
    • batch message reads
    • bulk fetch warmup
    • thread reconstruction by header message id
    • bulk tag updates
  • cache folder information with TTL to reduce repeated folder metadata calls
  • switch attachment base64 encoding to chunked conversion to avoid long blokcing threads
  • add the idle permission required for idle-state reconnect handling

Bridge

  • add WebSocket heartbeat handling to detect dead extension connections
  • least-permission CORS handling by validating allowed origins instead of replying with wildcard access
  • keep preflight handling explicit and reject disallowed origins

Why

Batterymaxxing.

Testing

Please run before merge:

npm test
npm run test:mcp

Checklist

✅ Runtime behavior improved in extension and bridge

✅ Docs updated for the new behavior/audit

✅ Test suites run locally before merge

Copilot AI and others added 16 commits July 28, 2026 17:48
…parallel message ops

- C1: Replace fixed RECONNECT_DELAY=3000 with exponential backoff
  (3s→6s→12s→24s→48s→60s cap, ±1s jitter); reset to 3s on connect.
  Drops idle wakeups from ~20/min to ~1/min within 2 minutes of bridge absence.

- M3: Add browser.idle.onStateChanged listener for macOS sleep/wake.
  On system wake (state→'active'), reset backoff and reconnect immediately
  rather than waiting out the full exponential delay.
  Add 'idle' permission to manifest.json.

- H1: Parallelize read-batch with Promise.all; each message's get()+getFull()
  also run concurrently — 2N sequential IPC calls → N concurrent pairs.

- H2: Parallelize thread reconstruction — all header-ID queries issued
  concurrently via Promise.all; O(1) Set deduplication replaces O(n) find().

- H3/H4: Add concurrentMap(concurrency=8) helper; replace serial per-message
  loops in bulk/tag, bulk/fetch, and messages/fetch folder path with it.

- docs: Update ENERGY_AUDIT_MACOS25.md with implementation status, updated
  compliance matrix, and proposed-but-not-implemented M2 (base64 loop) note.
fix(extension): exponential backoff reconnect, sleep/wake awareness, parallel message ops
…L cache

Co-authored-by: le-dawg <3172733+le-dawg@users.noreply.github.com>
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Fix extension reconnect/resume regressions and add folder-info TTL cache
Fixed 8 file(s) based on 6 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
fix: harden Thunderbird MCP lifecycle and search failures
@vitalio-sh

Copy link
Copy Markdown
Owner

Thank you @le-dawg — parts of this ship in 1.1.0 via #25, with you as co-author:

  • WebSocket ping/pong heartbeat and the CORS origin allowlist in the bridge
  • reconnect backoff and immediate reconnect on idle → active (idle permission)
  • bounded concurrency for read-batch, bulk tag/fetch, folder fetch and thread lookups; chunked base64 for attachments
  • email_search with filters only

Not taken, with reasons:

  • 30s folder-info cache: unread counts go stale right after mark-read, and Thunderbird is meant to be the source of truth
  • reconnect cap 60s → we used 15s so a restarted bridge doesn't wait a minute for the extension
  • per-call /bridge/status preflight: the bridge already answers 503 immediately when the extension is disconnected
  • SEARCH_UNHEALTHY / LIST_UNHEALTHY and the timeout !== 30000 heuristic: error-code contract change, and the docs (5000ms) disagreed with the code
  • error mapping that ignored the bridge's code (it would hide AUTH_REQUIRED from Enforce TB_AUTH_TOKEN on the bridge HTTP listener #21)
  • /debug/compose-capabilities, the 15s resume poll, and docs/ENERGY_AUDIT_MACOS25.md

Closing in favour of #25.

@vitalio-sh vitalio-sh closed this Sep 14, 2026
jctots pushed a commit to jctots/thunderbird-cli that referenced this pull request Sep 14, 2026
…; add heartbeat

A web page in the user's browser could drive the localhost bridge:

- Blind cross-origin POSTs executed even though CORS hid the response
  (verified: a text/plain POST /compose {send:true} from
  Origin: https://evil.example reached the extension).
- DNS rebinding let a hostile domain read responses same-origin.
- Any page could open ws://127.0.0.1:7701 and replace the extension
  socket, receiving every CLI request and answering with forged data.

The bridge now rejects HTTP requests with an Origin outside
TB_BRIDGE_CORS_ORIGINS, Host headers that are not an IP literal,
localhost, *.localhost, *.internal (host.docker.internal) or listed in
TB_BRIDGE_ALLOWED_HOSTS, and WebSocket handshakes from http(s)/file/null
origins. tb, tb-mcp and curl send no Origin header and are unaffected;
the extension connects from moz-extension://.

The CORS allowlist and WebSocket ping/pong heartbeat (drops sockets that
stop answering, e.g. after sleep) come from PR vitalio-sh#20.

test/bridge-security.mjs runs the real bridge: 27 checks, 16 of which
fail against the previous bridge.

Co-authored-by: le-dawg <3172733+le-dawg@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uoUWTHVsDJpMJCVNFZz5L
jctots pushed a commit to jctots/thunderbird-cli that referenced this pull request Sep 14, 2026
… bounded parallelism

Integrates the extension parts of PR vitalio-sh#4 and PR vitalio-sh#20, with fixes:

Thread (from vitalio-sh#4)
- Parse References / In-Reply-To / Message-ID from getRaw() (getFull()
  doesn't expose them) and strip angle brackets before
  messages.query({ headerMessageId }) — previously `tb thread` always
  returned an empty list. getFull() fallback now strips brackets too.
- Downstream replies found by subject are limited to exact normalized
  subject matches (Thunderbird's query is substring), exclude junk, and
  are labelled threadMatch: "subject" vs "references".
- thread-utils.js is loaded by the manifest and is the single source for
  the parsing helpers instead of a test-only copy.

Recent / search (from vitalio-sh#4)
- /recent honours accountId, and now unreadOnly as well; the CLI sent
  both but the extension ignored them.
- /messages/search accepts headerMessageId.

Connection (from vitalio-sh#20, reworked)
- Reconnect backoff 3s → 6s → 12s → 15s cap (PR used 60s, which would
  leave a restarted bridge waiting up to a minute), reset on connect,
  immediate retry on idle → active ("idle" permission).
- Socket handlers are bound to their own socket, so a late close from a
  replaced socket can't null the live one (main crashed on ws.send).

Throughput (from vitalio-sh#20)
- read-batch, fetch, bulk tag/fetch and thread lookups run through a
  shared queue capped at 8 concurrent operations, preserving order.
- Attachment base64 encoded in 32 KB chunks instead of byte by byte.

Not taken from vitalio-sh#20: the 30s folder-info cache (stale unread counts
right after mark-read; Thunderbird is the source of truth), the
/debug/compose-capabilities endpoint, and the 15s resume-poll interval.

test/extension.test.mjs loads the real background scripts in a vm with a
mocked messenger API (39 checks); against the previous extension the
thread, recent, search and reconnect checks fail.

Co-authored-by: Daniel Böckenhoff <dboe@ipp.mpg.de>
Co-authored-by: le-dawg <3172733+le-dawg@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uoUWTHVsDJpMJCVNFZz5L
jctots pushed a commit to jctots/thunderbird-cli that referenced this pull request Sep 14, 2026
…egration

email_search no longer requires `query` when at least one filter is
given (from PR vitalio-sh#20); with neither it fails with INVALID_ARGS instead of
running an unbounded search.

Not taken from PR vitalio-sh#20's client changes: the per-call /bridge/status
preflight (the bridge already answers 503 immediately when the extension
is disconnected), the new SEARCH_UNHEALTHY / LIST_UNHEALTHY codes and
`timeout !== 30000` override heuristic (error-code contract change whose
docs disagreed with the code), and error mapping that dropped the
bridge's own `code` (it would have hidden AUTH_REQUIRED from vitalio-sh#21).

Co-authored-by: le-dawg <3172733+le-dawg@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uoUWTHVsDJpMJCVNFZz5L
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.

4 participants