Wire the session_exit RPC to an actual caller - #104
Conversation
core/__init__.py's dispatcher has a session_exit handler that calls
run_light_consolidation (an FSRS decay/reinforcement tick over records
touched in the last hour) — but nothing in the package ever sent a
session_exit request. It's the light, frequent counterpart to the nightly
heavy pass, and per the Stop hook's own docstring ("Claude Code fires Stop
after EVERY assistant response... this hook therefore does two cheap
incremental things per response"), this is exactly the kind of per-response
work that hook is already designed to carry — it just never got a third
step added for this specific RPC.
Fix:
- New cmd_session_exit in cli/_capture.py, following the same pattern as
the existing cmd_session_start: sends the RPC with its own short
timeouts (3s connect / 15s read) and fails safe on any error, matching
the fail-safe contract of every other hook backend in this file.
- Registered as `iai-mcp session-exit --session-id ...`.
- iai-mcp-session-capture.sh (the Stop hook, shared across Claude Code,
Codex and Cursor) now calls it as a third step, after the existing
capture-turn-deferred catch-up and live-file rotation.
Verified against a live daemon: before the fix, zero cls_consolidation_run
events existed with mode "light" from any real session. After wiring this
in and calling the new CLI command, a genuine event appeared with the
correct session_id and mode, confirming the full
hook -> CLI -> RPC -> run_light_consolidation -> write_event path.
Added test_cmd_session_exit_daemon_unreachable.py covering the fail-safe
contract and the argparse registration, mirroring the existing
test_cmd_session_start_daemon_unreachable.py.
|
Thank you for tracing this all the way to the missing caller, and for verifying it against a live daemon rather than by reading. The wiring is clean and the fail-safe matches One problem with the placement. The Stop hook fires after every assistant response, not at session end, so a 100-turn session means 100 light consolidations, each recomputing FSRS over the same last-hour records. Repeated reinforcement over the same rows moves the stability curves, so "harmless" doesn't hold at that rate. Two ways out: call it where a session actually ends, or dedupe in time, once per N minutes per session. Either works for us, and rough cost numbers would help. Happy to merge once that's settled. |
…ssion Stop fires after every assistant response, not at session end, so a long session was calling run_light_consolidation -- and its unconditional per-record FSRS stability boost (_apply_fsrs has no guard against being applied twice to the same record in quick succession) -- on every single turn. A 100-turn session meant 100 recomputations of FSRS over the same last-hour records, each one moving the stability curves again. The RPC handler now tracks the last light-consolidation-pass time per session in a sidecar file and skips the pass (returning `deduped: true` and the remaining cooldown) if it ran within the last 5 minutes for that session -- matching the wrapper's own idle threshold for the WAKE -> DROWSY edge elsewhere in this project, so a burst of quick turns collapses to roughly one tick per idle-sized gap instead of one per response. The tmp write for the sidecar is PID-suffixed, consistent with CodeAbra#102. Confirmed against a live daemon: two immediate session_exit RPC calls for the same session_id produced exactly one cls_consolidation_run event (mode: light); the second call's raw response was `{"mode": "light", "deduped": true, "cooldown_remaining_sec": 266.3}`. Two tests added: one confirms a second call within the window is deduped and only one light-mode event exists for that session; one confirms the cooldown is per-session, not global (a different session_id is never blocked by another session's recent pass).
|
Good catch — you're right, and the fix was straightforward once you named the actual problem. Pushed a new commit: Went with dedupe-in-time, not detecting a real session boundary — the RPC handler now tracks the last light-pass timestamp per session in a sidecar and skips (returning Cost numbers you asked for: confirmed against a live daemon that two immediate |
Fixes #100.
core/__init__.py's dispatcher has asession_exithandler that callsrun_light_consolidation(an FSRS decay/reinforcement tick over records touched in the last hour) — but nothing in the package ever sent asession_exitrequest. It's the light, frequent counterpart to the nightly heavy pass, and per the Stop hook's own docstring ("Claude Code fires Stop after EVERY assistant response... this hook therefore does two cheap incremental things per response"), this is exactly the kind of per-response work that hook is already designed to carry — it just never got a third step added for this specific RPC.Fix
cmd_session_exitincli/_capture.py, following the same pattern as the existingcmd_session_start: sends the RPC with its own short timeouts (3s connect / 15s read) and fails safe on any error, matching the fail-safe contract of every other hook backend in this file.iai-mcp session-exit --session-id ....iai-mcp-session-capture.sh(the Stop hook, shared across Claude Code, Codex and Cursor) now calls it as a third step, after the existing capture-turn-deferred catch-up and live-file rotation.Verification
Verified against a live daemon: before the fix, zero
cls_consolidation_runevents existed withmode: "light"from any real session. After wiring this in and calling the new CLI command directly, a genuine event appeared with the correctsession_idandmode, confirming the full hook -> CLI -> RPC ->run_light_consolidation->write_eventpath.Test plan
test_cmd_session_exit_daemon_unreachable.py— covers the fail-safe contract (unreachable daemon ->rc == 0) and the argparse registration, mirroring the existingtest_cmd_session_start_daemon_unreachable.pyiai-mcp session-exit --session-id <test>produced a realcls_consolidation_runevent with the correct session_id andmode: "light"ruff check --select F,E9on changed files — no new issues