Skip to content

perf: dirty-flag polling for channel unread checks#653

Merged
laynepenney merged 3 commits intosprint-17from
apollo/dirty-flag-polling
Apr 10, 2026
Merged

perf: dirty-flag polling for channel unread checks#653
laynepenney merged 3 commits intosprint-17from
apollo/dirty-flag-polling

Conversation

@laynepenney
Copy link
Copy Markdown
Collaborator

Summary

  • Adds unread_flags table to channels SQLite DB for O(1) unread checks
  • channel_has_unread() returns in <1ms when nothing changed (single SQLite read vs JSONL scan)
  • Flag set on post/directive/pin writes for all other channel members
  • Flag cleared only by channel_read() (full read), NOT by channel_unread() (count-only)
  • Backward compatible: existing channel_unread() behavior unchanged

Changes

  • channel.py: Added unread_flags table schema, _set_dirty_flags() helper, channel_has_unread() function, dirty-flag set in _append_message and channel_pin, dirty-flag clear in channel_read
  • test_channel.py: 10 new tests covering write-flag-read-clear lifecycle

Premium boundary: this is OSS recall infrastructure. The dirty-flag optimization benefits all agents polling channels, reducing per-poll cost from O(channels x JSONL_size) to O(1).

Test plan

  • 10 new dirty-flag tests pass
  • Full channel test suite: 192 passed, 0 failed
  • Full repo test suite: 1972 passed, 19 skipped, 0 failures

Closes #638

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown
Contributor

Thank you for your contribution! Before we can merge this PR, we need you to sign our Contributor License Agreement.

To sign, please comment on this PR with the following exact text:

I have read the CLA Document and I hereby sign the CLA

You can also re-trigger the CLA check by commenting recheck.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@laynepenney
Copy link
Copy Markdown
Collaborator Author

QA review — LGTM.

Design is correct. Dirty flag separate from the read cursor is the right approach — O(1) check without changing existing unread semantics. Key invariants all hold: flag cleared only by channel_read() not channel_unread(), sender excluded from their own flags, join initializes clean, pin and directive both route through _append_message so they set flags correctly.

Tests are thorough. 10 tests cover the full lifecycle plus edge cases: count-only doesn't clear (critical invariant), multiple posts single clear, pin sets dirty, directive sets dirty, non-member gets empty dict. Full suite at 1972 passed confirms no regressions.

Minor note (non-blocking): _set_dirty_flags() opens a fresh DB connection rather than receiving one as a parameter. Works fine in practice (tests pass), but if _append_message ever holds an uncommitted write when it calls _set_dirty_flags, the two connections could serialize under SQLite WAL. Consider passing the connection in a future refactor.

Boundary declaration present and correct. Needs one more review.

@laynepenney
Copy link
Copy Markdown
Collaborator Author

this needs to merge into sprint-17 branch, not main

laynepenney and others added 3 commits April 10, 2026 18:11
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Agents polling for unread messages previously scanned every channel's
JSONL file and compared timestamps. This is O(messages) per poll for
what is usually a "nothing changed" answer.

Add an unread_flags table in the channels SQLite DB:
- On any write (_append_message, channel_pin), set dirty=1 for all
  other channel members
- channel_has_unread() checks the flag in a single SQLite read (~0.1ms)
- Flag is only cleared by channel_read() (full read), NOT by
  channel_unread() (count-only)
- Initialized clean on channel_join

10 tests covering the full write-flag-read-clear lifecycle.
1972 passed, 0 failed.

Closes #638

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@laynepenney laynepenney force-pushed the apollo/dirty-flag-polling branch from e967e78 to f25c0e8 Compare April 10, 2026 23:11
@laynepenney laynepenney changed the base branch from main to sprint-17 April 10, 2026 23:12
@laynepenney laynepenney merged commit bfb3d7e into sprint-17 Apr 10, 2026
9 of 10 checks passed
@laynepenney laynepenney deleted the apollo/dirty-flag-polling branch April 10, 2026 23:16
@github-actions github-actions bot locked and limited conversation to collaborators Apr 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: lightweight dirty-flag for channel polling instead of full unread scan

1 participant