Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This change

## [Unreleased]

### Added (v0.1.32 sync)
- `:on-event` optional handler in `create-session` and `resume-session` configs — zero-arg function registered on the session before the `session.create`/`session.resume` RPC is issued. Guarantees that early events emitted by the CLI during session creation (e.g. `session.start`) are delivered to the handler without being dropped. Equivalent to calling `subscribe-events` immediately after creation but executes earlier in the lifecycle (upstream PR #664).

### Changed (v0.1.32 sync)
- Session registration now occurs **before** the `session.create`/`session.resume` RPC call so events emitted during the RPC (e.g. `session.start`) are routed correctly and not dropped. Session ID is generated client-side (UUID) when not provided via `:session-id`. On RPC failure, registered session state is cleaned up automatically (upstream PR #664).

## [0.1.32.0] - 2026-03-10
### Added (v0.1.32 sync)
- `:agent` optional string parameter in `create-session` and `resume-session` configs — pre-selects a custom agent by name when the session starts. Must match a name in `:custom-agents`. Equivalent to calling `agent.select` after creation (upstream PR #722).
Expand Down
1 change: 1 addition & 0 deletions doc/reference/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ Create a client and session together, ensuring both are cleaned up on exit.
| `:on-user-input-request` | fn | Handler for `ask_user` requests (see below) |
| `:hooks` | map | Lifecycle hooks (see below) |
| `:agent` | string | Name of a custom agent to activate at session start. Must match a name in `:custom-agents`. Equivalent to calling `agent.select` after creation. |
| `:on-event` | fn | Optional event handler registered before `session.create` RPC. Called for every session event, including `session.start` which is emitted during session creation. Guarantees no early events are dropped (upstream PR #664). |

#### `resume-session`

Expand Down
87 changes: 59 additions & 28 deletions src/github/copilot_sdk/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -1192,18 +1192,19 @@
(:disable-resume? config) (assoc :disable-resume (:disable-resume? config))
true (assoc :env-value-mode "direct"))))

(defn- finalize-session
"Create a CopilotSession from an RPC result and config."
[client result config]
(let [session-id (:session-id result)
workspace-path (:workspace-path result)]
(session/create-session client session-id
{:tools (:tools config)
:on-permission-request (:on-permission-request config)
:on-user-input-request (:on-user-input-request config)
:hooks (:hooks config)
:workspace-path workspace-path
:config config})))
(defn- register-and-create-session!
"Register session state before RPC and return a handler map for post-RPC finalization.
The session is registered in client state so events dispatched during session.create/
session.resume RPC (e.g. session.start) are not dropped."
[client session-id config]
(session/create-session client session-id
{:tools (:tools config)
:on-permission-request (:on-permission-request config)
:on-user-input-request (:on-user-input-request config)
:hooks (:hooks config)
:on-event (:on-event config)
:workspace-path nil
:config config}))

(defn create-session
"Create a new conversation session.
Expand Down Expand Up @@ -1236,18 +1237,33 @@
- :hooks - Lifecycle hooks map (PR #269):
{:on-pre-tool-use, :on-post-tool-use, :on-user-prompt-submitted,
:on-session-start, :on-session-end, :on-error-occurred}
- :on-event - Optional event handler registered before session.create RPC so early
events (e.g. session.start) are not dropped (upstream PR #664).
Equivalent to calling (subscribe-events session) + processing the channel,
but guaranteed to receive events emitted during session creation.

Returns a CopilotSession."
[client config]
(log/debug "Creating session with config: " (select-keys config [:model :session-id]))
(validate-session-config! config)
(ensure-connected! client)
(let [{:keys [connection-io]} @(:state client)
params (build-create-session-params config)
result (proto/send-request! connection-io "session.create" params)
session (finalize-session client result config)]
(log/info "Session created: " (:session-id result))
session))
;; Generate UUID client-side so session can be registered before RPC (upstream PR #664)
session-id (or (:session-id config) (str (java.util.UUID/randomUUID)))
;; Register session state BEFORE the RPC so events dispatched during session.create
;; (e.g. session.start) are not dropped
_ (register-and-create-session! client session-id config)
params (assoc (build-create-session-params config) :session-id session-id)]
(try
(let [result (proto/send-request! connection-io "session.create" params)
workspace-path (:workspace-path result)
session (session/update-workspace-path! client session-id workspace-path)]
(log/info "Session created: " session-id)
session)
(catch Exception e
;; Clean up session state if RPC failed
(session/cleanup-failed-session! client session-id)
(throw e)))))

(defn resume-session
"Resume an existing session by ID.
Expand All @@ -1271,6 +1287,7 @@
- :reasoning-effort - Reasoning effort level: \"low\", \"medium\", \"high\", or \"xhigh\"
- :on-user-input-request - Handler for ask_user requests
- :hooks - Lifecycle hooks map
- :on-event - Optional event handler registered before session.resume RPC (upstream PR #664)

Returns a CopilotSession."
[client session-id config]
Expand All @@ -1288,10 +1305,16 @@
{:config config})))
(ensure-connected! client)
(let [{:keys [connection-io]} @(:state client)
params (build-resume-session-params session-id config)
result (proto/send-request! connection-io "session.resume" params)
session (finalize-session client result config)]
session))
;; Register session state BEFORE the RPC (upstream PR #664)
_ (register-and-create-session! client session-id config)
params (build-resume-session-params session-id config)]
(try
(let [result (proto/send-request! connection-io "session.resume" params)
workspace-path (:workspace-path result)]
(session/update-workspace-path! client session-id workspace-path))
(catch Exception e
(session/cleanup-failed-session! client session-id)
(throw e)))))

(defn <create-session
"Async version of create-session. Returns a channel that delivers a CopilotSession.
Expand All @@ -1316,17 +1339,22 @@
(validate-session-config! config)
(ensure-connected! client)
(let [{:keys [connection-io]} @(:state client)
params (build-create-session-params config)
session-id (or (:session-id config) (str (java.util.UUID/randomUUID)))
;; Register session state BEFORE the RPC (upstream PR #664)
_ (register-and-create-session! client session-id config)
params (assoc (build-create-session-params config) :session-id session-id)
rpc-ch (proto/send-request connection-io "session.create" params)]
(go
(when-let [response (<! rpc-ch)]
(if-let [err (:error response)]
(do (log/error "<create-session RPC error: " err)
(do (session/cleanup-failed-session! client session-id)
(log/error "<create-session RPC error: " err)
(ex-info (str "Failed to create session: " (:message err))
{:error err}))
(let [result (:result response)
session (finalize-session client result config)]
(log/info "Session created (async): " (:session-id result))
workspace-path (:workspace-path result)
session (session/update-workspace-path! client session-id workspace-path)]
(log/info "Session created (async): " session-id)
session))))))

(defn <resume-session
Expand Down Expand Up @@ -1363,17 +1391,20 @@
{:config config})))
(ensure-connected! client)
(let [{:keys [connection-io]} @(:state client)
;; Register session state BEFORE the RPC (upstream PR #664)
_ (register-and-create-session! client session-id config)
params (build-resume-session-params session-id config)
rpc-ch (proto/send-request connection-io "session.resume" params)]
(go
(when-let [response (<! rpc-ch)]
(if-let [err (:error response)]
(do (log/error "<resume-session RPC error: " err)
(do (session/cleanup-failed-session! client session-id)
(log/error "<resume-session RPC error: " err)
(ex-info (str "Failed to resume session: " (:message err))
{:error err :session-id session-id}))
(let [result (:result response)
session (finalize-session client result config)]
session))))))
workspace-path (:workspace-path result)]
(session/update-workspace-path! client session-id workspace-path)))))))

(defn list-sessions
"List all available sessions.
Expand Down
36 changes: 34 additions & 2 deletions src/github/copilot_sdk/session.clj
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@

(defn create-session
"Create a new session. Internal use - called by client.
Initializes session state in client's atom and returns a CopilotSession handle."
[client session-id {:keys [tools on-permission-request on-user-input-request hooks workspace-path config]}]
Initializes session state in client's atom and returns a CopilotSession handle.
If :on-event is provided, starts a background go-loop that calls the handler
for each event dispatched to this session (registered before session.create RPC)."
[client session-id {:keys [tools on-permission-request on-user-input-request hooks workspace-path config on-event]}]
(log/debug "Creating session: " session-id)
(let [event-chan (chan (async/sliding-buffer 4096))
event-mult (mult event-chan)
Expand All @@ -70,10 +72,40 @@
{:event-chan event-chan
:event-mult event-mult
:send-lock send-lock}))))
;; If on-event handler provided, start a go-loop to call it for each event.
;; Registered before session.create RPC so early events (e.g. session.start) are not dropped.
(when on-event
(let [tap-ch (chan (async/sliding-buffer 1024))]
(tap event-mult tap-ch)
(go-loop []
(when-let [event (<! tap-ch)]
(try
(on-event event)
(catch Exception e
(log/warn "on-event handler error for session " session-id ": " (ex-message e))))
(recur)))))
(log/debug "Session created: " session-id)
;; Return lightweight handle
(->CopilotSession session-id workspace-path client)))

(defn update-workspace-path!
"Update the workspace-path in session state after a successful session.create/session.resume RPC.
Returns a new CopilotSession record with the updated workspace-path."
[client session-id workspace-path]
(update-session! client session-id assoc :workspace-path workspace-path)
(->CopilotSession session-id workspace-path client))

(defn cleanup-failed-session!
"Clean up session state after a failed session.create/session.resume RPC.
Closes the event channel (which stops any on-event go-loop) and removes session state."
[client session-id]
(when-let [{:keys [event-chan]} (session-io client session-id)]
(close! event-chan))
(swap! (:state client)
(fn [s] (-> s
(update :sessions dissoc session-id)
(update :session-io dissoc session-id)))))

(defn dispatch-event!
"Dispatch an event to all subscribers via the mult. Called by client notification router.
Events are dropped (with warning) if the session event buffer is full."
Expand Down
11 changes: 7 additions & 4 deletions src/github/copilot_sdk/specs.clj
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,17 @@

(s/def ::client-name ::non-blank-string)

;; Early event handler registered before session.create/session.resume RPC (upstream PR #664)
(s/def ::on-event fn?)

(def session-config-keys
#{:session-id :client-name :model :tools :system-message
:available-tools :excluded-tools :provider
:on-permission-request :streaming? :mcp-servers
:custom-agents :config-dir :skill-directories
:disabled-skills :large-output :infinite-sessions
:reasoning-effort :on-user-input-request :hooks
:working-directory :agent})
:working-directory :agent :on-event})

(s/def ::session-config
(closed-keys
Expand All @@ -225,15 +228,15 @@
::custom-agents ::config-dir ::skill-directories
::disabled-skills ::large-output ::infinite-sessions
::reasoning-effort ::on-user-input-request ::hooks
::working-directory ::agent])
::working-directory ::agent ::on-event])
session-config-keys))

(def ^:private resume-session-config-keys
#{:client-name :model :tools :system-message :available-tools :excluded-tools
:provider :streaming? :on-permission-request
:mcp-servers :custom-agents :config-dir :skill-directories
:disabled-skills :infinite-sessions :reasoning-effort
:on-user-input-request :hooks :working-directory :disable-resume? :agent})
:on-user-input-request :hooks :working-directory :disable-resume? :agent :on-event})

(s/def ::resume-session-config
(closed-keys
Expand All @@ -242,7 +245,7 @@
::provider ::streaming?
::mcp-servers ::custom-agents ::config-dir ::skill-directories
::disabled-skills ::infinite-sessions ::reasoning-effort
::on-user-input-request ::hooks ::working-directory ::disable-resume? ::agent])
::on-user-input-request ::hooks ::working-directory ::disable-resume? ::agent ::on-event])
resume-session-config-keys))

;; -----------------------------------------------------------------------------
Expand Down