[IJAI-1133]: fix(client): don't fail session/update for sessions the client never connected to - #124
Open
forketyfork wants to merge 6 commits into
Conversation
Rizzen
requested changes
Aug 21, 2026
Comment on lines
+42
to
+54
| public class Client( | ||
| public val protocol: Protocol, | ||
| @property:UnstableApi | ||
| public val globalElicitationHandler: GlobalElicitationHandler? = null | ||
| public val globalElicitationHandler: GlobalElicitationHandler?, | ||
| @property:UnstableApi | ||
| public val globalSessionUpdateHandler: GlobalSessionUpdateHandler? | ||
| ) { | ||
| // kept for backwards ABI compatibility after adding globalSessionUpdateHandler | ||
| public constructor( | ||
| protocol: Protocol, | ||
| globalElicitationHandler: GlobalElicitationHandler? = null, | ||
| ) : this(protocol, globalElicitationHandler, null) | ||
|
|
Collaborator
There was a problem hiding this comment.
Why two ctors instead of single ctor with two nullable optional args?
forketyfork
added a commit
to forketyfork/kotlin-sdk
that referenced
this pull request
Aug 21, 2026
…defaulted Addresses review comment on PR agentclientprotocol#124: collapse the 3-arg primary constructor and the ABI-compatibility secondary constructor into one primary constructor with `globalElicitationHandler` and `globalSessionUpdateHandler` both defaulted to null. The compat constructor is unnecessary here: commit 1e717bb added `globalElicitationHandler` to this same class as a defaulted parameter and simply dropped `Client(Protocol)` from the API dump. Keeping the extra constructor also forced callers to pass `globalElicitationHandler = null` explicitly to reach the new handler.
…connected to Issue: session/update (including session_info_update) notifications for a sessionId the client hasn't called session/new/load/resume for were failing with "Session <id> not found", even though such sessions can legitimately live on the server and be created/updated from another IDE window, the web, or another machine (JetBrains/IJAI-1133). Solution: split the session-holder lookup into a non-throwing findSessionHolder used by the session/update notification handler (which now drops updates for unknown sessions instead of failing) and the existing throwing getOrCreateSessionHolder, still used by session-scoped request handlers that must fail fast on an unknown session. Added a regression test that reproduces the crash pre-fix and verifies updates keep flowing afterward.
…obal handler Issue: after the previous fix, session/update notifications for a session this client never called session/new/load/resume for stopped crashing, but were silently dropped - leaving no way for a client to build features like a live session/list view that reacts to sessions created/updated elsewhere (another IDE window, the web, another machine). Solution: add GlobalSessionUpdateHandler, an optional Client constructor callback mirroring the existing GlobalElicitationHandler pattern. When a session/update arrives for a session with no registered holder, it is now routed to this handler (if set) instead of just being logged and dropped.
Co-authored-by: forketyfork <1592872+forketyfork@users.noreply.github.com>
…lobal handler Issue: PR review comment (#2, discussion_r3810926817) found that an update for a genuinely unconnected session arriving while an unrelated session is being initialized gets buffered into a speculative placeholder holder (findSessionHolder's initializingSessionsCount>0 branch exists to tolerate the newSession/loadSession response race). When that initialization completes without ever claiming the placeholder, it is reaped as "hanging" and completed exceptionally - silently discarding whatever was queued in it, bypassing GlobalSessionUpdateHandler entirely. Solution: add ClientSessionHolder.completeExceptionallyAndDrainQueue, which drains any buffered notifications before closing the channel, and use it in withInitializingSession's hanging-session cleanup to forward each drained update to globalSessionUpdateHandler (if registered) for that session id. createSession's own failure path keeps calling plain completeExceptionally unchanged, since those buffered notifications belong to a session the client itself tried and failed to create, not an unconnected one.
…defaulted Addresses review comment on PR agentclientprotocol#124: collapse the 3-arg primary constructor and the ABI-compatibility secondary constructor into one primary constructor with `globalElicitationHandler` and `globalSessionUpdateHandler` both defaulted to null. The compat constructor is unnecessary here: commit 1e717bb added `globalElicitationHandler` to this same class as a defaulted parameter and simply dropped `Client(Protocol)` from the API dump. Keeping the extra constructor also forced callers to pass `globalElicitationHandler = null` explicitly to reach the new handler.
forketyfork
force-pushed
the
fix/session-update-unconnected-session
branch
from
August 21, 2026 20:05
fd11145 to
78ac806
Compare
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.
Issue
https://youtrack.jetbrains.com/issue/IJAI-1133
session/update(includingsession_info_update) notifications fail withacpFail("Session <id> not found")when the client hasn't calledsession/new/session/load/session/resumefor thatsessionId— unless another session happens to be mid-initialization at that exact moment.This is too strict: sessions can live on the server and be created, updated, or deleted from another IDE window, the web, or another machine. An agent notifying this client about such a session (e.g. a status change for a session shown in a session list) is not a protocol violation, so the client shouldn't treat it as an error — and once it isn't an error, the client should be able to observe it (e.g. to keep a
session/list-rendered list live without polling).Solution
In
Client.kt, the session-holder lookup used bygetOrCreateSessionHolderwas extracted into a new non-throwingfindSessionHolder(sessionId): ClientSessionHolder?.session/updatenotification handler now callsfindSessionHolderand, when it returnsnull(session unknown and not currently being initialized), no longer fails.getOrCreateSessionHolderkeeps its old throwing behavior on top offindSessionHolder, and is unchanged for every other call site (FsReadTextFile,TerminalCreate,ElicitationCreate, etc.) — those are genuine session-scoped requests from the agent and must still fail fast if they reference a session the client never established.GlobalSessionUpdateHandler, an optionalClientconstructor callback mirroring the existingGlobalElicitationHandlerpattern. When an update arrives for a session with no registered holder, it's routed to this handler if one is set (e.g. to add a newly-appeared session to a locally displayed list); if none is set, the update is dropped with a debug log, same as before.findSessionHolderstill speculatively creates a placeholder holder whileinitializingSessionsCount > 0, so an update for an unrelated server-side session arriving in that window gets buffered rather than routed to the handler. When the last initialization finishes and nosession/new/session/loadclaimed that id,withInitializingSession's hanging-session cleanup now usesClientSessionHolder.completeExceptionallyAndDrainQueueto drain the buffered notifications before closing the holder, and forwards each toglobalSessionUpdateHandler— the id is unconnected after all, exactly as if no initialization had been in progress when its updates arrived.createSession's own failure path still uses the plaincompleteExceptionally: those buffered notifications belong to a session this client tried and failed to create, not to an unconnected one.Client's constructor takes bothglobalElicitationHandlerandglobalSessionUpdateHandleras optional parameters defaulted tonull. As with1e717bb, which introducedglobalElicitationHandlerthe same way, this shifts the published JVM constructor descriptors, soacp/api/acp.apiis regenerated: the 3-arg constructor and itsDefaultConstructorMarkersynthetic replace the previous 2-arg pair.Context
session/listhook, follow-up PR [IJAI-1134] feat(agent,protocol): support suspend-based paginated session/list #123.Covered by automated tests in
acp/src/jvmTest:ClientSessionUpdateForUnconnectedSessionTest(unknown-session updates no longer fail),ClientGlobalSessionUpdateHandlerTest(delivery to the global handler), andClientHangingSessionUpdateDeliveryTest(an update for an unrelated session fired while asession/newis in flight still reaches the handler).