claude-agent-sdk 0.16.9 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +1 -1
- data/docs/sessions.md +79 -0
- data/lib/claude_agent_sdk/command_builder.rb +4 -0
- data/lib/claude_agent_sdk/fiber_boundary.rb +14 -2
- data/lib/claude_agent_sdk/message_parser.rb +1 -0
- data/lib/claude_agent_sdk/query.rb +62 -8
- data/lib/claude_agent_sdk/session_mutations.rb +248 -63
- data/lib/claude_agent_sdk/session_resume.rb +444 -0
- data/lib/claude_agent_sdk/session_store.rb +353 -0
- data/lib/claude_agent_sdk/session_summary.rb +188 -0
- data/lib/claude_agent_sdk/sessions.rb +460 -7
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +84 -0
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +295 -0
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +218 -0
- data/lib/claude_agent_sdk/types.rb +43 -11
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +292 -62
- metadata +9 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abd1a08c12369ca6417cc28946a153f2f641ba38e1770026ff53ae36465da34c
|
|
4
|
+
data.tar.gz: c1a35168f601b9bf8f6680cf66565c6f06f26f0c4817d2e5a8afbe2923b0935d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fe246f00316a5d6d704e30d4a6df1d732be3d494d47fc222fe44738d7fc17914fd281a36b6fdf5200ddf69e7a8f2b4f6d471ec48799d0005375097a0d535ef8
|
|
7
|
+
data.tar.gz: 82cb35a8b05262b407014fd704eef12ac00f13bbb15f428c40e402b4fc6eb9576fd61b6f328bc4b00dc356365e2a16c18f6e5f97156877c0f7f4c23b499f8e96
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.17.0] - 2026-06-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Pluggable `SessionStore` adapter subsystem** — mirror Claude Code session transcripts to external storage (S3/Redis/Postgres/…) and resume from it, at parity with the Python SDK (#837 and follow-ups) and TypeScript SDK. The subprocess still writes to local disk; the adapter receives a secondary copy.
|
|
14
|
+
- **`SessionStore`** base class (6-method contract: `append`/`load` required; `list_sessions`/`list_session_summaries`/`delete`/`list_subkeys` optional, probed via `SessionStore.implements?` so duck-typed adapters need not subclass) and **`InMemorySessionStore`** reference implementation. All keys/entries cross the boundary as Hashes with **string keys** (JSON-round-trip safe for JSONB/Redis backends).
|
|
15
|
+
- **`ClaudeAgentSDK::Testing.run_session_store_conformance(make_store)`** — a 15-contract behavioral test suite for adapter authors (shipped in the gem, framework-agnostic; raises `ConformanceError` on violation).
|
|
16
|
+
- **Live mirroring** (on both `ClaudeAgentSDK.query` and `ClaudeAgentSDK::Client`): setting `ClaudeAgentOptions#session_store` emits `--session-mirror`; a `TranscriptMirrorBatcher` coalesces the CLI's `transcript_mirror` frames per file and flushes to `SessionStore#append` on each `result` (or 500-entry / 1 MiB overflow). `session_store_flush: "eager"` flushes after every frame. Append ordering is preserved across concurrent flushes (fiber-aware `Async::Semaphore`); the user store call runs on a `FiberBoundary` thread bounded by a fixed 60 s send timeout (`TranscriptMirrorBatcher::SEND_TIMEOUT_SECONDS`; `load_timeout_ms` bounds resume-materialization store calls instead); failures retry (3 attempts, 0.2/0.8 s backoff) then surface as a **`MirrorErrorMessage`** on the message stream. A failing store never disrupts the session (the local transcript is already durable).
|
|
17
|
+
- **Resume from store** (both entry points): pairing `session_store` with `resume`/`continue_conversation` materializes the session (and its subagent transcripts) from the store into a temp `CLAUDE_CONFIG_DIR` before spawn, repoints the subprocess at it, and cleans it up after the subprocess exits (on disconnect, one-shot teardown, **and** reactor cancellation). Store-supplied subagent subpaths are validated against path traversal; copied `.credentials.json` has its single-use `refreshToken` redacted; macOS Keychain credentials are bridged when needed.
|
|
18
|
+
- **Store-backed reads** (counterparts to the local readers): `ClaudeAgentSDK.list_sessions_from_store` (summary fast-path + gap-fill + pagination; a single failing row degrades to an empty summary rather than aborting the listing), `.get_session_info_from_store`, `.get_session_messages_from_store`, `.list_subagents_from_store`, `.get_subagent_messages_from_store`.
|
|
19
|
+
- **Store-backed mutations** (counterparts to the local mutators): `ClaudeAgentSDK.rename_session_via_store`, `.tag_session_via_store`, `.delete_session_via_store` (a no-op on WORM/append-only stores that don't implement `#delete`), and `.fork_session_via_store` (the UUID-remap fork transform run directly over the loaded entries — no JSONL round-trip; the fallback title is derived from the source's `customTitle`/`aiTitle`/first prompt).
|
|
20
|
+
- **Reference adapters** under `examples/session_stores/` (S3, Redis, Postgres) — copy-in implementations, each validated against `run_session_store_conformance`. Backend client gems live in the optional `:examples` Bundler group so a default install stays dependency-free.
|
|
21
|
+
- **`ClaudeAgentSDK.import_session_to_store`** — replay a local on-disk session (and subagents) into a store for migration / mirror-gap backfill.
|
|
22
|
+
- **`ClaudeAgentSDK.project_key_for_directory`** and **`.fold_session_summary`** helpers; `ClaudeAgentOptions` gains `session_store`, `session_store_flush` (`"batched"`/`"eager"`), and `load_timeout_ms`. `SessionStore` + `enable_file_checkpointing`, or `continue_conversation` without `list_sessions`, are rejected at connect with a clear error.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `Client#connect` now fully tears the client down (subprocess reaped, temp resume dir removed) when any part of connect fails — previously a failure while sending the initial prompt could leave a half-connected client behind. Matches the Python/TypeScript SDKs.
|
|
26
|
+
- A negative `limit:` now returns `[]` consistently across every session reader (`list_sessions`, `get_session_messages`, and all store-backed counterparts) instead of raising `ArgumentError` from an internal `Array#first` on some paths.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- `fork_session` / `fork_session_via_store` now fall back to the documented `"Forked session (fork)"` title for sessions with no title and no extractable first prompt (previously wrote a literal `" (fork)"`).
|
|
30
|
+
|
|
31
|
+
## [0.16.10] - 2026-06-04
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- `ClaudeAgentOptions#strict_mcp_config` — forwarded as `--strict-mcp-config`. When `true`, the CLI uses **only** the MCP servers passed via `mcp_servers`, ignoring project `.mcp.json`, user/global settings, and plugin-provided servers, for a fully deterministic server set. Defaults to `false`. (Parity with [Python SDK #915](https://github.com/anthropics/claude-agent-sdk-python/pull/915))
|
|
35
|
+
- `ClaudeAgentOptions#include_hook_events` — forwarded as `--include-hook-events`. When `true`, the CLI emits hook lifecycle events (PreToolUse, PostToolUse, Stop, etc.) into the message stream. The parser already maps these to `HookStartedMessage` / `HookProgressMessage` / `HookResponseMessage` (the CLI simply never emitted them without this flag). Defaults to `false`. (Parity with [Python SDK #917](https://github.com/anthropics/claude-agent-sdk-python/pull/917))
|
|
36
|
+
- `SandboxNetworkConfig#denied_domains` (`deniedDomains`) and `#allow_mach_lookup` (`allowMachLookup`) — completes the sandbox network allowlist field set. `denied_domains` blocks domains even when matched by `allowed_domains`; `allow_mach_lookup` is a macOS-only list of XPC/Mach service names to allow (supports a trailing wildcard). Completes [Python SDK #893](https://github.com/anthropics/claude-agent-sdk-python/pull/893) — `denied_domains` and `allow_mach_lookup` were the last two fields the Ruby port had not yet landed.
|
|
37
|
+
- **Orphaned-subprocess cleanup**: `SubprocessCLITransport` now tracks live CLI subprocesses in a class-level, mutex-guarded `Set` and registers an `at_exit` handler that sends `SIGTERM` to any still running when the parent Ruby process exits. This prevents leaked `claude` processes when callers crash or exit before reaching `#close`. The handler skips already-exited processes (`Process::Waiter#alive?`) and swallows errors so it never interrupts interpreter shutdown. (Parity with [Python SDK #916](https://github.com/anthropics/claude-agent-sdk-python/pull/916))
|
|
38
|
+
|
|
10
39
|
## [0.16.9] - 2026-05-25
|
|
11
40
|
|
|
12
41
|
### Fixed
|
data/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Add this line to your application's Gemfile:
|
|
|
68
68
|
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
|
|
69
69
|
|
|
70
70
|
# Or use a stable version from RubyGems
|
|
71
|
-
gem 'claude-agent-sdk', '~> 0.
|
|
71
|
+
gem 'claude-agent-sdk', '~> 0.17.0'
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Then `bundle install`, or install directly: `gem install claude-agent-sdk`.
|
data/docs/sessions.md
CHANGED
|
@@ -99,3 +99,82 @@ ClaudeAgentSDK.query(
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
`resume_session_at` requires `resume`; the SDK raises `ArgumentError` from `CommandBuilder` when this constraint is violated, matching the underlying CLI's validation but surfacing it synchronously in the caller's stack.
|
|
102
|
+
|
|
103
|
+
## Mirroring to a `SessionStore`
|
|
104
|
+
|
|
105
|
+
By default Claude Code writes session transcripts to local disk under
|
|
106
|
+
`CLAUDE_CONFIG_DIR`. A **`SessionStore`** adapter mirrors that transcript to
|
|
107
|
+
external storage (S3, Redis, Postgres, …) so sessions survive beyond the local
|
|
108
|
+
machine and can be resumed elsewhere. The subprocess still writes locally; the
|
|
109
|
+
adapter receives a secondary copy and resume can rehydrate from it.
|
|
110
|
+
|
|
111
|
+
Set `session_store:` on the options — it works on **both** `ClaudeAgentSDK.query`
|
|
112
|
+
and `ClaudeAgentSDK::Client`:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
store = ClaudeAgentSDK::InMemorySessionStore.new # or your own adapter
|
|
116
|
+
|
|
117
|
+
ClaudeAgentSDK.query(
|
|
118
|
+
prompt: 'Hello!',
|
|
119
|
+
options: ClaudeAgentSDK::ClaudeAgentOptions.new(session_store: store)
|
|
120
|
+
) { |message| } # transcript_mirror frames are appended to the store as they stream
|
|
121
|
+
|
|
122
|
+
# Resume later from the store (no local JSONL needed):
|
|
123
|
+
ClaudeAgentSDK.query(
|
|
124
|
+
prompt: 'Continue',
|
|
125
|
+
options: ClaudeAgentSDK::ClaudeAgentOptions.new(session_store: store, resume: 'previous-session-id')
|
|
126
|
+
) { |message| }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Relevant options: `session_store`, `session_store_flush` (`"batched"` default, or
|
|
130
|
+
`"eager"` to flush after every frame), and `load_timeout_ms` (per store call
|
|
131
|
+
during resume materialization, default `60_000`).
|
|
132
|
+
|
|
133
|
+
> **Store-backed resume runs against a bare temp `CLAUDE_CONFIG_DIR`.** Only the
|
|
134
|
+
> transcript plus `.credentials.json` (redacted) and `.claude.json` are
|
|
135
|
+
> materialized into it — user-scope `settings.json` (hooks, `permissions`),
|
|
136
|
+
> user `CLAUDE.md`, `agents/`, `skills/`, and `plugins/` from your real config
|
|
137
|
+
> dir are **not** visible to the subprocess, so a store-backed resume can
|
|
138
|
+
> behave differently from a plain `resume:` of the same session. Project-level
|
|
139
|
+
> `.claude/*` still applies (it resolves from `cwd`), and hooks/options passed
|
|
140
|
+
> programmatically via `ClaudeAgentOptions` are unaffected. This matches the
|
|
141
|
+
> Python and TypeScript SDKs.
|
|
142
|
+
|
|
143
|
+
### Implementing an adapter
|
|
144
|
+
|
|
145
|
+
Subclass `ClaudeAgentSDK::SessionStore` (or duck-type it). Only `#append` and
|
|
146
|
+
`#load` are required; `#list_sessions`, `#delete`, `#list_subkeys`, and
|
|
147
|
+
`#list_session_summaries` are optional and probed via `SessionStore.implements?`.
|
|
148
|
+
Validate your adapter with the shipped, framework-agnostic conformance harness:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
require 'claude_agent_sdk/testing/session_store_conformance'
|
|
152
|
+
ClaudeAgentSDK::Testing.run_session_store_conformance(-> { MyStore.new(...) })
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Copy-in reference adapters for **S3, Redis, and Postgres** live in
|
|
156
|
+
[`examples/session_stores/`](../examples/session_stores/README.md), each with a
|
|
157
|
+
production checklist.
|
|
158
|
+
|
|
159
|
+
### Store-backed helpers
|
|
160
|
+
|
|
161
|
+
The browsing/mutation helpers above have store-backed counterparts that take a
|
|
162
|
+
`session_store:` and operate on the store instead of local disk:
|
|
163
|
+
|
|
164
|
+
- Reads: `list_sessions_from_store`, `get_session_info_from_store`,
|
|
165
|
+
`get_session_messages_from_store`, `list_subagents_from_store`,
|
|
166
|
+
`get_subagent_messages_from_store`. Unlike the disk readers (where a nil
|
|
167
|
+
`directory:` searches every project directory), the store helpers key every
|
|
168
|
+
read by `project_key` and a nil `directory:` defaults to the **current
|
|
169
|
+
working directory** — the `SessionStore` interface has no way to enumerate
|
|
170
|
+
project keys (parity with the Python SDK).
|
|
171
|
+
- Mutations: `rename_session_via_store`, `tag_session_via_store`,
|
|
172
|
+
`delete_session_via_store` (a no-op on append-only stores without `#delete`),
|
|
173
|
+
`fork_session_via_store`.
|
|
174
|
+
- Migration: `import_session_to_store` replays a local on-disk session (and its
|
|
175
|
+
subagents) into a store.
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
ClaudeAgentSDK.rename_session_via_store(session_store: store, session_id: '550e8400-...', title: 'Renamed')
|
|
179
|
+
forked = ClaudeAgentSDK.fork_session_via_store(session_store: store, session_id: '550e8400-...')
|
|
180
|
+
```
|
|
@@ -277,6 +277,10 @@ module ClaudeAgentSDK
|
|
|
277
277
|
cmd.push("--include-partial-messages") if @options.include_partial_messages
|
|
278
278
|
cmd.push("--fork-session") if @options.fork_session
|
|
279
279
|
cmd.push("--bare") if @options.bare
|
|
280
|
+
cmd.push("--include-hook-events") if @options.include_hook_events
|
|
281
|
+
cmd.push("--strict-mcp-config") if @options.strict_mcp_config
|
|
282
|
+
# When a session_store is set, ask the CLI to emit transcript_mirror frames.
|
|
283
|
+
cmd.push("--session-mirror") if @options.session_store
|
|
280
284
|
end
|
|
281
285
|
|
|
282
286
|
def append_plugins(cmd)
|
|
@@ -28,15 +28,27 @@ module ClaudeAgentSDK
|
|
|
28
28
|
# so SDK loops yielding user callbacks must keep loop control outside the
|
|
29
29
|
# invoked block (see `Client#receive_response`).
|
|
30
30
|
module FiberBoundary
|
|
31
|
+
# Raised by .invoke when a timeout-bounded call exceeds its allotted time.
|
|
32
|
+
# The worker thread is abandoned (cancellation is best-effort; the
|
|
33
|
+
# in-flight call may still complete).
|
|
34
|
+
class JoinTimeout < StandardError; end
|
|
35
|
+
|
|
31
36
|
module_function
|
|
32
37
|
|
|
33
38
|
# Run the given block on a plain thread when a Fiber scheduler is active.
|
|
34
39
|
# Returns the block's value. Exceptions propagate to the caller.
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
#
|
|
41
|
+
# With +timeout+ (seconds) the thread hop happens unconditionally — even
|
|
42
|
+
# without a scheduler — so the bound is enforced in plain synchronous code
|
|
43
|
+
# too; JoinTimeout is raised when it expires.
|
|
44
|
+
def invoke(timeout: nil, &block)
|
|
45
|
+
return block.call if timeout.nil? && !Fiber.scheduler
|
|
37
46
|
|
|
38
47
|
thread = Thread.new(&block)
|
|
39
48
|
thread.report_on_exception = false
|
|
49
|
+
return thread.value if timeout.nil?
|
|
50
|
+
raise JoinTimeout, "timed out after #{timeout}s" unless thread.join(timeout)
|
|
51
|
+
|
|
40
52
|
thread.value
|
|
41
53
|
end
|
|
42
54
|
end
|
|
@@ -87,6 +87,7 @@ module ClaudeAgentSDK
|
|
|
87
87
|
'status' => StatusMessage,
|
|
88
88
|
'api_retry' => APIRetryMessage,
|
|
89
89
|
'local_command_output' => LocalCommandOutputMessage,
|
|
90
|
+
'mirror_error' => MirrorErrorMessage,
|
|
90
91
|
'hook_started' => HookStartedMessage,
|
|
91
92
|
'hook_progress' => HookProgressMessage,
|
|
92
93
|
'hook_response' => HookResponseMessage,
|
|
@@ -56,6 +56,7 @@ module ClaudeAgentSDK
|
|
|
56
56
|
@initialized = false
|
|
57
57
|
@closed = false
|
|
58
58
|
@initialization_result = nil
|
|
59
|
+
@transcript_mirror_batcher = nil
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
# Initialize control protocol if in streaming mode
|
|
@@ -150,6 +151,28 @@ module ClaudeAgentSDK
|
|
|
150
151
|
@task = parent.async { read_messages }
|
|
151
152
|
end
|
|
152
153
|
|
|
154
|
+
# Install the transcript-mirror batcher fed by `transcript_mirror` frames
|
|
155
|
+
# (Client mode with a session_store). nil disables mirroring.
|
|
156
|
+
def set_transcript_mirror_batcher(batcher)
|
|
157
|
+
@transcript_mirror_batcher = batcher
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Synthesize a `mirror_error` system message and put it on the SDK message
|
|
161
|
+
# stream so consumers learn a mirror batch was dropped after exhausting
|
|
162
|
+
# retries. Non-blocking: the message queue is unbounded, so unlike the
|
|
163
|
+
# Python SDK there is no buffer-full drop path.
|
|
164
|
+
def report_mirror_error(key, error)
|
|
165
|
+
session_id = key && (key['session_id'] || key[:session_id])
|
|
166
|
+
@message_queue.enqueue(
|
|
167
|
+
type: 'system',
|
|
168
|
+
subtype: 'mirror_error',
|
|
169
|
+
error: error,
|
|
170
|
+
key: key,
|
|
171
|
+
uuid: SecureRandom.uuid,
|
|
172
|
+
session_id: session_id || ''
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
153
176
|
private
|
|
154
177
|
|
|
155
178
|
def control_request_timeout_seconds
|
|
@@ -200,10 +223,20 @@ module ClaudeAgentSDK
|
|
|
200
223
|
task = request_id ? @inflight_control_request_tasks[request_id] : nil
|
|
201
224
|
task&.stop
|
|
202
225
|
next
|
|
226
|
+
when 'transcript_mirror'
|
|
227
|
+
# session_store mirror frame — fed to the batcher, never surfaced to
|
|
228
|
+
# consumers. camelCase on the wire; transport symbolizes keys.
|
|
229
|
+
@transcript_mirror_batcher&.enqueue(message[:filePath] || message[:file_path], message[:entries] || [])
|
|
230
|
+
next
|
|
203
231
|
else
|
|
204
|
-
if message[:type] == 'result'
|
|
205
|
-
|
|
206
|
-
|
|
232
|
+
if message[:type] == 'result'
|
|
233
|
+
# Flush the mirror before signaling/yielding the result so a
|
|
234
|
+
# consumer observing the result sees an up-to-date store for the turn.
|
|
235
|
+
flush_transcript_mirror
|
|
236
|
+
unless @first_result_received
|
|
237
|
+
@first_result_received = true
|
|
238
|
+
@first_result_condition.signal
|
|
239
|
+
end
|
|
207
240
|
end
|
|
208
241
|
# Regular SDK messages go to the queue
|
|
209
242
|
@message_queue.enqueue(message)
|
|
@@ -232,12 +265,30 @@ module ClaudeAgentSDK
|
|
|
232
265
|
# Put error in queue so iterators can handle it
|
|
233
266
|
@message_queue.enqueue({ type: 'error', error: e })
|
|
234
267
|
ensure
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
268
|
+
# Catch entries from a turn that ended without a `result` (early EOF /
|
|
269
|
+
# transport error) so they aren't dropped. The flush can suspend (lock
|
|
270
|
+
# acquire / thread join), so Async::Stop delivered mid-flush would skip
|
|
271
|
+
# the rest of this block — the nested ensure guarantees the signal and
|
|
272
|
+
# the end sentinel (which have no suspension points) are still delivered,
|
|
273
|
+
# mirroring the Python port's shielded flush + send_nowait sentinel.
|
|
274
|
+
begin
|
|
275
|
+
flush_transcript_mirror
|
|
276
|
+
ensure
|
|
277
|
+
unless @first_result_received
|
|
278
|
+
@first_result_received = true
|
|
279
|
+
@first_result_condition.signal
|
|
280
|
+
end
|
|
281
|
+
# Always signal end of stream
|
|
282
|
+
@message_queue.enqueue({ type: 'end' })
|
|
238
283
|
end
|
|
239
|
-
|
|
240
|
-
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Flush the transcript-mirror batcher, swallowing errors — a mirror failure
|
|
287
|
+
# must never propagate into the read loop or its teardown.
|
|
288
|
+
def flush_transcript_mirror
|
|
289
|
+
@transcript_mirror_batcher&.flush
|
|
290
|
+
rescue StandardError => e
|
|
291
|
+
warn "Claude SDK: transcript mirror flush failed: #{e.message}"
|
|
241
292
|
end
|
|
242
293
|
|
|
243
294
|
def handle_control_response(message)
|
|
@@ -976,6 +1027,9 @@ module ClaudeAgentSDK
|
|
|
976
1027
|
# Close the query and transport
|
|
977
1028
|
def close
|
|
978
1029
|
@closed = true
|
|
1030
|
+
# Final mirror flush BEFORE stopping the read task, so the last turn's
|
|
1031
|
+
# entries reach the store. #close on the batcher never raises.
|
|
1032
|
+
@transcript_mirror_batcher&.close
|
|
979
1033
|
@task&.stop
|
|
980
1034
|
@transport.close
|
|
981
1035
|
end
|