fix(session): emit session:start once per session, not per execute() — v1.6.0 - #84
Merged
Brian Krabach (bkrabach) merged 1 commit intoMay 18, 2026
Conversation
Restores pre-Rust contract: session lifecycle events fire at session initialization, not on every Session::execute() call. The Rust port (d2826b4, 2026-02-14) introduced session:start emission inside execute(), causing every interactive prompt to re-fire the event. Hook authors (hooks-routing, hook-context-intelligence, others) had registered handlers expecting once-per-session semantics — those handlers were re-running expensive work (HTTP calls to list_models, provider enumeration) on every turn, contributing ~7-13s wall-clock cost per interactive prompt. Fix: add lifecycle_event_emitted AtomicBool guard to Session struct. claim_lifecycle_event() returns true exactly once per session lifetime; all three emission sites are guarded by this claim. - crates/amplifier-core/src/session.rs: add lifecycle_event_emitted field + claim_lifecycle_event() method; guard emit in execute(). - bindings/python/src/session.rs: guard emit in PySession::execute() via claim_lifecycle_event() on the inner Session. - python/amplifier_core/session.py: guard emit in AmplifierSession execute() via _lifecycle_event_emitted bool flag. - session:start fires once when fresh session first executes - session:resume fires once when resumed session first executes - session:end continues to fire once at cleanup (unchanged) - execute() becomes pure orchestrator delegation after first turn Tests: - NEW: session_start_emitted_once_across_multiple_execute_calls (3 execute() calls → exactly 1 session:start; was failing with 3) - NEW: session_resume_emitted_once_across_multiple_execute_calls - UPDATED: execute_emits_session_start_event now calls execute() twice and asserts exactly 1 event (not just "at least 1") - UPDATED: execute_emits_session_resume_for_resumed_session same - NEW Python: TestSessionStartOncePerSession — 3 multi-turn tests documenting once-per-session contract for the Python binding Rust test suite: 469 tests, 0 failures. Python multi-turn tests are RED against installed v1.2.6 (expected — they verify the contract this fix delivers; GREEN after build/install). Version bumped 1.5.3 → 1.6.0. Co-authored-by: Amplifier <noreply@anthropic.com>
Brian Krabach (bkrabach)
deleted the
fix/session-start-per-session-not-per-execute
branch
May 18, 2026 15:53
Brian Krabach (bkrabach)
added a commit
that referenced
this pull request
May 19, 2026
Two assert_eq! macros in mod tests had multi-line arg formatting that rustfmt --check rejected. PR #84 landed despite the failing format check because admin-merge bypassed CI. This restores green Rust Core CI. Both diffs collapse: assert_eq!( - start_count, - 1, + start_count, 1, "…" ); No functional change. Co-authored-by: Amplifier <amplifier@microsoft.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root Cause
Rust port on 2026-02-14 (commit
d2826b4) addedsession:startemission insideSession::execute(). Every interactive prompt re-fired the event. Hook authors (hooks-routing,hook-context-intelligence, others) registered handlers expecting once-per-session semantics — those handlers were re-running expensive work (HTTP calls tolist_models, provider enumeration, agent iteration) on every turn, contributing ~7–13s wall-clock cost per interactive prompt.Evidence
session:startin 3-turn interactive sessionsamplifier_core/session.pyfrom commitf6d8ed6(2025-10-08) had NOhooks.emitinexecute()— confirming this was a regression introduced during the Rust portd2826b4message explicitly stated lifecycle events would be in the kernel, but the port embedded them inexecute()rather thaninitialize(), breaking the once-per-session invariantFix
Added
lifecycle_event_emitted: AtomicBoolfield onSession; newclaim_lifecycle_event()method atomically returnstrueonce per session lifetime. All three emission sites (Rust kernelexecute(), PyO3 binding, pure-Python session) guarded by this claim.Tests
session_start_emitted_once_across_multiple_execute_calls,session_resume_emitted_once_across_multiple_execute_calls)test_session_start_dedup.py::TestSessionStartOncePerSession(will turn green after build/install)Behavior Change
session:startandsession:resumenow fire exactly once per session lifetime, not perexecute()call. This restores the pre-Rust contract.session:endcontinues to fire once at cleanup (unchanged).execute()becomes pure orchestrator delegation.Performance Impact
Removes ~7–13 seconds of per-turn wall-clock cost in interactive sessions where bundle includes routing-matrix or context-intelligence hooks.