diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e5a68..f3cd648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/doc/reference/API.md b/doc/reference/API.md index 033f30b..8dc258f 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -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` diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj index ddac748..aadb90b 100644 --- a/src/github/copilot_sdk/client.clj +++ b/src/github/copilot_sdk/client.clj @@ -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. @@ -1236,6 +1237,10 @@ - :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] @@ -1243,11 +1248,22 @@ (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. @@ -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] @@ -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 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." diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj index edf81dc..c9bf114 100644 --- a/src/github/copilot_sdk/specs.clj +++ b/src/github/copilot_sdk/specs.clj @@ -207,6 +207,9 @@ (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 @@ -214,7 +217,7 @@ :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 @@ -225,7 +228,7 @@ ::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 @@ -233,7 +236,7 @@ :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 @@ -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)) ;; -----------------------------------------------------------------------------