Skip to content

[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
agentclientprotocol:masterfrom
forketyfork:fix/session-update-unconnected-session
Open

[IJAI-1133]: fix(client): don't fail session/update for sessions the client never connected to#124
forketyfork wants to merge 6 commits into
agentclientprotocol:masterfrom
forketyfork:fix/session-update-unconnected-session

Conversation

@forketyfork

@forketyfork forketyfork commented Aug 19, 2026

Copy link
Copy Markdown

Issue

https://youtrack.jetbrains.com/issue/IJAI-1133

session/update (including session_info_update) notifications fail with acpFail("Session <id> not found") when the client hasn't called session/new / session/load / session/resume for that sessionId — 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 by getOrCreateSessionHolder was extracted into a new non-throwing findSessionHolder(sessionId): ClientSessionHolder?.

  • The session/update notification handler now calls findSessionHolder and, when it returns null (session unknown and not currently being initialized), no longer fails.
  • getOrCreateSessionHolder keeps its old throwing behavior on top of findSessionHolder, 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.
  • Added GlobalSessionUpdateHandler, an optional Client constructor callback mirroring the existing GlobalElicitationHandler pattern. 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.
  • Closed the gap where the initialization race swallowed such updates. findSessionHolder still speculatively creates a placeholder holder while initializingSessionsCount > 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 no session/new / session/load claimed that id, withInitializingSession's hanging-session cleanup now uses ClientSessionHolder.completeExceptionallyAndDrainQueue to drain the buffered notifications before closing the holder, and forwards each to globalSessionUpdateHandler — 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 plain completeExceptionally: those buffered notifications belong to a session this client tried and failed to create, not to an unconnected one.

Client's constructor takes both globalElicitationHandler and globalSessionUpdateHandler as optional parameters defaulted to null. As with 1e717bb, which introduced globalElicitationHandler the same way, this shifts the published JVM constructor descriptors, so acp/api/acp.api is regenerated: the 3-arg constructor and its DefaultConstructorMarker synthetic replace the previous 2-arg pair.

Context

Covered by automated tests in acp/src/jvmTest: ClientSessionUpdateForUnconnectedSessionTest (unknown-session updates no longer fail), ClientGlobalSessionUpdateHandlerTest (delivery to the global handler), and ClientHangingSessionUpdateDeliveryTest (an update for an unrelated session fired while a session/new is in flight still reaches the handler).

@forketyfork forketyfork changed the title fix(client): don't fail session/update for sessions the client never connected to [IJAI-1133]: fix(client): don't fail session/update for sessions the client never connected to Aug 19, 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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why two ctors instead of single ctor with two nullable optional args?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason, removed.

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.
forketyfork and others added 6 commits August 21, 2026 22:00
…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
forketyfork force-pushed the fix/session-update-unconnected-session branch from fd11145 to 78ac806 Compare August 21, 2026 20:05
@forketyfork
forketyfork requested a review from Rizzen August 21, 2026 20:05
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.

3 participants