src/Internal/Runtime/LifecycleHandler.php:118 handles a Resume envelope by calling EventLog::replayAfter($after) and forwarding every yielded envelope onto the requesting session's transport. The underlying query at src/Store/EventLog.php:151 is SELECT payload_json FROM events WHERE rowid > :rowid ORDER BY rowid ASC and is not constrained by session_id, principal, or any other ownership column. When $msg->afterMessageId is null or an empty string the loop replays the entire append-only event log from the beginning, including envelopes that were recorded for other sessions.
A peer can therefore send Resume(after_message_id: "") and harvest every event ever logged across all sessions — tool inputs, telemetry, human prompts, granted leases, error payloads — even when those events belong to a different principal. The runtime advertises Resume as a session-scoped reconnection primitive in docs/guides/resume.md:29, and the auth invariant statement there explicitly promises "Only resume sessions under the same authenticated principal." The current code does not enforce that invariant. The events table already carries a session_id column and an events_session_idx index, so filtering by the requesting session is cheap.
Fix prompt: Scope the resume replay to envelopes the requesting session is entitled to see. In LifecycleHandler::handleResume, pass the calling Session's id and principal to a new EventLog::replayAfterForSession() (or add a session filter parameter to replayAfter()) and emit DATA_LOSS / PERMISSION_DENIED for cross-session after_message_id values. Add an integration test in tests/Integration/ResumeTest.php that opens two sessions, records events on each, and asserts that a Resume(after_message_id: "") on session A receives only envelopes from session A.
src/Internal/Runtime/LifecycleHandler.php:118handles aResumeenvelope by callingEventLog::replayAfter($after)and forwarding every yielded envelope onto the requesting session's transport. The underlying query atsrc/Store/EventLog.php:151isSELECT payload_json FROM events WHERE rowid > :rowid ORDER BY rowid ASCand is not constrained bysession_id, principal, or any other ownership column. When$msg->afterMessageIdisnullor an empty string the loop replays the entire append-only event log from the beginning, including envelopes that were recorded for other sessions.A peer can therefore send
Resume(after_message_id: "")and harvest every event ever logged across all sessions — tool inputs, telemetry, human prompts, granted leases, error payloads — even when those events belong to a different principal. The runtime advertises Resume as a session-scoped reconnection primitive indocs/guides/resume.md:29, and the auth invariant statement there explicitly promises "Only resume sessions under the same authenticated principal." The current code does not enforce that invariant. Theeventstable already carries asession_idcolumn and anevents_session_idxindex, so filtering by the requesting session is cheap.Fix prompt: Scope the resume replay to envelopes the requesting session is entitled to see. In
LifecycleHandler::handleResume, pass the callingSession's id and principal to a newEventLog::replayAfterForSession()(or add a session filter parameter toreplayAfter()) and emitDATA_LOSS/PERMISSION_DENIEDfor cross-sessionafter_message_idvalues. Add an integration test intests/Integration/ResumeTest.phpthat opens two sessions, records events on each, and asserts that aResume(after_message_id: "")on session A receives only envelopes from session A.