fix(kanban): snap notify-sub cursor to current MAX(id) at creation - #29915
fix(kanban): snap notify-sub cursor to current MAX(id) at creation#29915briandevans wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR prevents notification subscriptions from replaying backlogged terminal task events by snapping new subscription cursors to the current task_events max id, and updates/extends tests to assert the new cursor semantics (including idempotent re-subscribe behavior).
Changes:
- Update
add_notify_subto initializelast_event_idfor newly-inserted subscriptions toMAX(task_events.id)for the task. - Adjust existing tests that previously assumed
last_event_id == 0to instead assert against the snapped cursor baseline. - Add regression tests covering “late subscribe” catch-up behavior and idempotent subscriptions not rewinding the cursor.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
hermes_cli/kanban_db.py |
Snap new notify subscription cursor to MAX(task_events.id) to avoid replay storms. |
tests/hermes_cli/test_kanban_notify.py |
Add/adjust tests validating cursor snapping and idempotent behavior. |
tests/hermes_cli/test_kanban_core_functionality.py |
Update expectation to use snapped cursor as the baseline instead of 0. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| sub = kb.list_notify_subs(conn, tid)[0] | ||
| assert sub["last_event_id"] == max_id_before, ( | ||
| "Expected new sub to start at the current MAX(id); got " | ||
| f"{sub['last_event_id']} vs {max_id_before}" | ||
| ) |
| kinds=("completed", "blocked", "gave_up", "crashed", "timed_out"), | ||
| ) | ||
| sub_before = kb.list_notify_subs(conn, tid)[0] | ||
| cursor_before = sub_before["last_event_id"] |
| # Pre-populate task_events so a fresh sub created now would otherwise | ||
| # see them all on the next tick. | ||
| kb._append_event(conn, tid, kind="completed") | ||
| kb._append_event(conn, tid, kind="blocked") |
|
@copilot All three findings addressed in commit e7727de8c:
The two failing CI tests ( |
e7727de to
c5df9ff
Compare
c5df9ff to
73c9ed5
Compare
73c9ed5 to
5f3490f
Compare
5f3490f to
bdc03b5
Compare
bdc03b5 to
d73e6c1
Compare
d73e6c1 to
2aee372
Compare
2aee372 to
7afcba5
Compare
7afcba5 to
150dd33
Compare
150dd33 to
95b4d86
Compare
95b4d86 to
1b27515
Compare
`add_notify_sub` inserted rows with `last_event_id = 0` (schema default). A sub created on an already-active task therefore claimed every prior terminal event on the next `_kanban_notifier_watcher` tick. Issue NousResearch#29905 reports 100+ "✔ Kanban done" messages firing in a single burst when 27 subs sat at `last_event_id=0` across a gateway restart. Snap the cursor to `MAX(id) FROM task_events WHERE task_id = ?` after the INSERT OR IGNORE so newly-inserted subs start caught up; only events generated AFTER subscription fire notifications. Existing rows are unaffected — the snap runs only when `cur.rowcount > 0`, so re-subscribing never rewinds an advancing cursor. Regression coverage in `tests/hermes_cli/test_kanban_notify.py`: pre-existing terminal events must NOT replay, fresh events MUST still deliver, and idempotent resubscription MUST NOT move the cursor.
Address Copilot review on NousResearch#29915: - Wrap `_append_event` calls in `with kb.write_txn(conn):` per the function's documented "called from within an already-open txn" contract. - Cast `sub["last_event_id"]` to `int` before numeric comparisons — `sqlite3.Row` dict materialization may surface the column as `str` under some sqlite typing/coercion paths, in which case `> 0` raises `TypeError` and `== max_id_before` is silently False. The rest of the file already follows this `int()` cast convention.
1b27515 to
996895b
Compare
|
Closing to keep my queue focused — this P3 kanban notify-cursor snap has been idle ~22 days with no maintainer engagement and the kanban SQLite area is actively owned by other contributors. Happy to reopen if maintainers want it. Thanks! |
What does this PR do?
add_notify_subpreviously insertedkanban_notify_subsrows withlast_event_id = 0(the schema default). A subscription created on an already-active task therefore replayed every prior terminal event on the next_kanban_notifier_watchertick — issue #29905 reports a 100+ message burst at gateway boot caused by 27 stale subs atlast_event_id=0.After this PR,
add_notify_subsnaps the cursor toCOALESCE(MAX(id) FROM task_events WHERE task_id = ?, 0)inside the samewrite_txnas the INSERT, so a new sub starts caught up. Only events generated after subscription fire notifications. The snap is gated oncur.rowcount > 0, so the existingINSERT OR IGNOREsemantics are preserved: a re-subscription against an existing(task, platform, chat, thread)tuple is still a no-op and never rewinds the advancing cursor.Scope is limited to the root cause (the missing snap on creation). Existing subs already sitting at
last_event_id=0are out of scope here — the reporter's workaround SQL handles those, and a separate on-connect migration would be a larger, riskier change to bundle.Related Issue
Fixes #29905
Type of Change
Changes Made
hermes_cli/kanban_db.py—add_notify_subnow snapslast_event_idto the currentMAX(id)overtask_eventsfor the task after a newly-inserted row.tests/hermes_cli/test_kanban_notify.py— two new regression tests: backlogged terminal events must NOT replay for a newly-created sub; fresh post-subscription events MUST still deliver; idempotent re-subscription MUST NOT rewind the cursor.tests/hermes_cli/test_kanban_notify.py::test_notifier_skips_subscription_owned_by_other_profile— invariant updated from "cursor must equal 0" to "cursor must equal the snapped baseline" (same intent: wrong-profile watcher must not advance the cursor).tests/hermes_cli/test_kanban_core_functionality.py::test_notify_claim_is_single_owner_and_rewindable—old_cursorbaseline is now the snapped value rather than the hard-coded0; the rest of the single-owner / rewind invariants are unchanged.How to Test
uv run --with pytest --with pytest-xdist --with pytest-asyncio --with pytest-timeout \ python3 -m pytest tests/hermes_cli/test_kanban_notify.py \ tests/gateway/test_kanban_notifier.py \ tests/hermes_cli/test_kanban_db.py \ tests/hermes_cli/test_kanban_core_functionality.py -vManual repro per the issue: create a task, append a couple of terminal events directly to
task_events, then calladd_notify_sub— onmainthe next watcher tick fires once per backlogged event; with this PR no notifications fire until a new event arrives.Checklist
Code
fix(kanban):)Documentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A