Skip to content

Wire the session_exit RPC to an actual caller - #104

Open
tom-shields-stitch wants to merge 2 commits into
CodeAbra:mainfrom
tom-shields-stitch:fix/session-exit-light-consolidation-unwired
Open

Wire the session_exit RPC to an actual caller#104
tom-shields-stitch wants to merge 2 commits into
CodeAbra:mainfrom
tom-shields-stitch:fix/session-exit-light-consolidation-unwired

Conversation

@tom-shields-stitch

Copy link
Copy Markdown

Fixes #100.

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.

Verification

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 directly, a genuine event appeared with the correct session_id and mode, confirming the full hook -> CLI -> RPC -> run_light_consolidation -> write_event path.

Test plan

  • New test_cmd_session_exit_daemon_unreachable.py — covers the fail-safe contract (unreachable daemon -> rc == 0) and the argparse registration, mirroring the existing test_cmd_session_start_daemon_unreachable.py
  • Manual round-trip against a live daemon: iai-mcp session-exit --session-id <test> produced a real cls_consolidation_run event with the correct session_id and mode: "light"
  • ruff check --select F,E9 on changed files — no new issues

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.
@CodeAbra

Copy link
Copy Markdown
Owner

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 session_start.

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).
@tom-shields-stitch

Copy link
Copy Markdown
Author

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 deduped: true + remaining cooldown) if it ran within the last 5 minutes for that session. Picked 5 minutes to match the wrapper's own idle threshold for the WAKE → DROWSY edge elsewhere in this project, so it's not an arbitrary number.

Cost numbers you asked for: confirmed against a live daemon that two immediate session_exit calls for the same session now produce exactly one cls_consolidation_run event instead of two, with the second call's response coming back as {"mode": "light", "deduped": true, "cooldown_remaining_sec": 266.3}. Two new tests cover the dedup itself and that it's per-session (a different session_id is never blocked by another session's recent pass).

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.

session_exit RPC handler exists but nothing ever sends the request

2 participants