Looking at the extension runner, I don't see any built-in reentrancy protection. The emit method just iterates through handlers and calls them.
Let me think about the good solution options:
-
Add reentrancy guard in setSessionName: Track if we're currently emitting this event and skip re-emission. Simple but specific to this one event.
-
Make it a "before" event with transform capability: Change to session_before_metadata_change that can return a modified name. This is the cleanest API but more work.
-
Document that handlers must not call setSessionName: Weakest solution, relies on users reading docs.
-
Add a general reentrancy guard in ExtensionRunner: More complex, would need to track which events are currently being processed per-type.
Looking at the existing events:
session_before_switch, session_before_fork, session_before_compact, session_before_tree - these are "before" events that can cancel or modify behavior
session_switch, session_fork, session_compact, session_tree - these are "after" events for notification
The pattern in the codebase is:
- "before" events can cancel/modify
- "after" events are for notification
So session_metadata_change follows the "after" pattern correctly. But if the use case is to transform the name, it should be a "before" event.
The simplest fix that's consistent with the codebase patterns would be:
- Keep
session_metadata_change as a notification-only event (don't call setSessionName from handler)
- OR add a reentrancy guard in
setSessionName
Let me check if any other "after" events have similar reentrancy concerns. For example, what if a session_switch handler calls ctx.newSession()? That would also be problematic.
Actually, looking at the ExtensionContext for event handlers, they don't have newSession, fork, etc.
Event handlers receive a limited ExtensionContext that doesn't expose setSessionName, while the pi object in extensions does have it. So from within a session_metadata_change handler, I'd need to call pi.setSessionName to trigger reentrancy—it's not directly available through the event context itself.
The pi object gets captured in closure and remains accessible to all handlers, making reentrancy possible. A straightforward solution would be adding a guard flag to setSessionName that returns early if already in progress, preventing recursive calls from propagating further.
However, using void for fire-and-forget emission means the finally block executes before async handlers complete, so the flag gets reset prematurely. Converting to an async approach where the method awaits the emission would properly maintain the flag throughout the entire operation, ensuring handlers can't trigger reentrancy even if they're asynchronous.
The trade-off is that callers would need to await the method, though the existing call sites like handleNameCommand and the extension API's setSessionName could accommodate this change.
However, the extension API defines setSessionName as returning void, and changing it to async would alter the type signature—a breaking change. Instead, I could keep the method synchronous and use fire-and-forget emission, or document that calling setSessionName from within a session_metadata_change handler produces undefined behavior.
The most practical approach is adding a reentrancy guard while maintaining the sync interface, though this only prevents immediate reentry rather than delayed async callbacks.