claude-agent-sdk 0.33.0 → 0.34.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 +81 -0
- data/README.md +1 -1
- data/docs/cli-installer.md +17 -9
- data/docs/client.md +1 -1
- data/docs/configuration.md +5 -5
- data/docs/errors.md +4 -3
- data/docs/mcp-servers.md +22 -0
- data/docs/observability.md +6 -0
- data/docs/rails.md +2 -0
- data/docs/sessions.md +66 -15
- data/lib/claude_agent_sdk/cli_installer.rb +34 -18
- data/lib/claude_agent_sdk/command_builder.rb +11 -3
- data/lib/claude_agent_sdk/configuration.rb +54 -2
- data/lib/claude_agent_sdk/errors.rb +11 -3
- data/lib/claude_agent_sdk/fiber_boundary.rb +42 -3
- data/lib/claude_agent_sdk/instrumentation/otel.rb +21 -2
- data/lib/claude_agent_sdk/query.rb +146 -62
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +56 -4
- data/lib/claude_agent_sdk/session_mutations.rb +41 -16
- data/lib/claude_agent_sdk/session_resume.rb +112 -39
- data/lib/claude_agent_sdk/session_store.rb +19 -3
- data/lib/claude_agent_sdk/session_summary.rb +5 -5
- data/lib/claude_agent_sdk/sessions.rb +123 -55
- data/lib/claude_agent_sdk/streaming.rb +0 -8
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +319 -54
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +13 -3
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +77 -18
- data/lib/claude_agent_sdk/types.rb +130 -36
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +49 -44
- metadata +16 -10
|
@@ -88,6 +88,13 @@ module ClaudeAgentSDK
|
|
|
88
88
|
|
|
89
89
|
UUID_RE = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
90
90
|
|
|
91
|
+
# Subagent ids as the CLI writes them (agent-<id>.jsonl): hex ids and
|
|
92
|
+
# prefixed forms like `aprompt_suggestion-1a2b3c`. The store readers
|
|
93
|
+
# synthesize `subagents/agent-<agent_id>` subpaths from caller input, so
|
|
94
|
+
# anything that could re-route a path-, prefix-, or URL-shaped adapter key
|
|
95
|
+
# ('/', '\', '..', '%', NUL, whitespace) is rejected at the boundary.
|
|
96
|
+
AGENT_ID_RE = /\A[A-Za-z0-9._-]+\z/
|
|
97
|
+
|
|
91
98
|
# Transcript entry types that participate in conversation reads. One shared
|
|
92
99
|
# constant for the disk (parse_jsonl_entries) and store
|
|
93
100
|
# (filter_transcript_entries) paths so the two read paths can't drift when
|
|
@@ -230,7 +237,8 @@ module ClaudeAgentSDK
|
|
|
230
237
|
# which reads only top-level keys — disagreed. A line that doesn't parse
|
|
231
238
|
# (truncated at the head/tail window edge) keeps the raw-scan value: its
|
|
232
239
|
# top-level shape can't be checked, and dropping it would regress the
|
|
233
|
-
# common case of a true entry cut by the 64KB window.
|
|
240
|
+
# common case of a true entry cut by the 64KB window. Unverified blanks
|
|
241
|
+
# cannot clear a previously verified value: they may be nested tool inputs.
|
|
234
242
|
def extract_top_level_string_field(text, key, last: false)
|
|
235
243
|
positions = field_match_positions(text, key)
|
|
236
244
|
positions.reverse! if last
|
|
@@ -247,7 +255,8 @@ module ClaudeAgentSDK
|
|
|
247
255
|
next # parseable line without a top-level string value: nested/false match
|
|
248
256
|
end
|
|
249
257
|
value = extract_json_string_value(text, value_start)
|
|
250
|
-
|
|
258
|
+
value = presence(unescape_json_string(value)) if value
|
|
259
|
+
return value if value
|
|
251
260
|
end
|
|
252
261
|
nil
|
|
253
262
|
end
|
|
@@ -306,16 +315,60 @@ module ClaudeAgentSDK
|
|
|
306
315
|
# Python's `x or None` for the summary/title fallback chains: Ruby's ||
|
|
307
316
|
# treats "" as truthy, so a CLI title-clearing entry ({"customTitle":""})
|
|
308
317
|
# would win over a real first prompt and then fail the summary presence
|
|
309
|
-
# check — silently dropping the whole session from disk listings
|
|
310
|
-
#
|
|
311
|
-
#
|
|
318
|
+
# check — silently dropping the whole session from disk listings.
|
|
319
|
+
# Whitespace-only counts as blank because the final gate strips. The ONE
|
|
320
|
+
# definition for both summary paths (SessionSummary calls it too): a
|
|
321
|
+
# second copy that only rejected "" let the store path list an invisible
|
|
322
|
+
# whitespace summary the disk path hid.
|
|
312
323
|
def presence(val)
|
|
313
324
|
return nil if val.nil?
|
|
314
|
-
|
|
325
|
+
# An invalidly encoded String (JSON.parse accepts raw invalid UTF-8
|
|
326
|
+
# inside strings) is unusable metadata, and String#strip would raise.
|
|
327
|
+
return nil if val.is_a?(String) && (!val.valid_encoding? || val.strip.empty?)
|
|
315
328
|
|
|
316
329
|
val
|
|
317
330
|
end
|
|
318
331
|
|
|
332
|
+
# Boundary checks for caller-supplied ids. A non-String id gets exactly
|
|
333
|
+
# the malformed-id answer (nil / [] / ArgumentError, per API) instead of
|
|
334
|
+
# a NoMethodError from deep inside (`123.match?`, `123.empty?`) — and so
|
|
335
|
+
# does an invalidly encoded String, on which the regexp match itself
|
|
336
|
+
# raises ArgumentError ("invalid byte sequence").
|
|
337
|
+
def valid_session_id?(session_id)
|
|
338
|
+
session_id.is_a?(String) && session_id.valid_encoding? && session_id.match?(UUID_RE)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def valid_agent_id?(agent_id)
|
|
342
|
+
agent_id.is_a?(String) && agent_id.valid_encoding? && agent_id.match?(AGENT_ID_RE) &&
|
|
343
|
+
!%w[. ..].include?(agent_id)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Adapters contractually report mtime as an epoch-ms Numeric (the
|
|
347
|
+
# conformance suite asserts it), but SQL timestamps naturally arrive as
|
|
348
|
+
# ISO-8601 Strings through JSON. Unary minus on a String is String#-@
|
|
349
|
+
# (frozen-string dedup), so String mtimes sorted lexicographically
|
|
350
|
+
# ASCENDING — oldest first, so `limit` cut off the newest sessions and
|
|
351
|
+
# --continue resumed the oldest — and mixed Integer/String lists raised a
|
|
352
|
+
# bare ArgumentError. Coerce defensively wherever an adapter mtime is
|
|
353
|
+
# ordered or compared: numeric strings and ISO-8601 both order
|
|
354
|
+
# correctly; anything else sorts last rather than crashing.
|
|
355
|
+
def sortable_mtime(value)
|
|
356
|
+
case value
|
|
357
|
+
when Numeric then value
|
|
358
|
+
when String then Float(value, exception: false) || parse_iso_timestamp_ms(value) || 0
|
|
359
|
+
else 0
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Sort key shared by every session listing, disk and store: newest first,
|
|
364
|
+
# ties broken by session_id ascending. sort_by is not stable, so without
|
|
365
|
+
# the secondary key equal mtimes (coarse adapter clocks, bulk imports)
|
|
366
|
+
# ordered arbitrarily between calls and offset/limit paging could skip or
|
|
367
|
+
# repeat sessions; one key also keeps the two paths in the same order.
|
|
368
|
+
def listing_sort_key(mtime, session_id)
|
|
369
|
+
[-sortable_mtime(mtime), session_id.to_s]
|
|
370
|
+
end
|
|
371
|
+
|
|
319
372
|
# Extract the first meaningful user prompt from the head of a JSONL file
|
|
320
373
|
def extract_first_prompt_from_head(head)
|
|
321
374
|
command_fallback = nil
|
|
@@ -380,25 +433,39 @@ module ClaudeAgentSDK
|
|
|
380
433
|
|
|
381
434
|
head, tail = read_head_tail(file_path, stat.size)
|
|
382
435
|
|
|
383
|
-
|
|
384
|
-
return nil if sidechain_first_line?(head.lines.first || '')
|
|
436
|
+
return nil if sidechain_head?(head, stat.size > LITE_READ_BUF_SIZE)
|
|
385
437
|
|
|
386
438
|
build_session_info(file_path, head, tail, stat, project_path)
|
|
387
439
|
rescue StandardError
|
|
388
440
|
nil
|
|
389
441
|
end
|
|
390
442
|
|
|
391
|
-
# Sidechain classification
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
395
|
-
#
|
|
396
|
-
#
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
443
|
+
# Sidechain classification reads the top-level key of the FIRST PARSEABLE
|
|
444
|
+
# entry — the entry the store fold classifies from (it sets is_sidechain
|
|
445
|
+
# once, from the first Hash entry; a store never holds an unparseable line
|
|
446
|
+
# since import skips them). The old raw substring scan also matched
|
|
447
|
+
# "isSidechain":true nested inside a structured field, and classifying
|
|
448
|
+
# strictly from line one let a corrupt/blank/non-object first line make
|
|
449
|
+
# the two paths disagree about whether the session exists. So blank,
|
|
450
|
+
# unparseable, and non-object lines are skipped — except a final line cut
|
|
451
|
+
# by the head window: it isn't corrupt, just unseen, so it can't be
|
|
452
|
+
# shape-checked and keeps the substring heuristic.
|
|
453
|
+
def sidechain_head?(head, window_truncated)
|
|
454
|
+
lines = head.lines
|
|
455
|
+
lines.each_with_index do |line, idx|
|
|
456
|
+
# (An invalidly encoded line isn't blank — and strip would raise on it.)
|
|
457
|
+
next if line.valid_encoding? && line.strip.empty?
|
|
458
|
+
|
|
459
|
+
begin
|
|
460
|
+
entry = JSON.parse(line)
|
|
461
|
+
rescue StandardError
|
|
462
|
+
next unless window_truncated && idx == lines.length - 1 && !line.end_with?("\n")
|
|
463
|
+
|
|
464
|
+
return line.include?('"isSidechain":true') || line.include?('"isSidechain": true')
|
|
465
|
+
end
|
|
466
|
+
return entry['isSidechain'] == true if entry.is_a?(Hash)
|
|
467
|
+
end
|
|
468
|
+
false
|
|
402
469
|
end
|
|
403
470
|
|
|
404
471
|
def read_head_tail(file_path, size)
|
|
@@ -417,17 +484,17 @@ module ClaudeAgentSDK
|
|
|
417
484
|
|
|
418
485
|
def build_session_info(file_path, head, tail, stat, project_path)
|
|
419
486
|
# User-set title (customTitle) wins over AI-generated title (aiTitle).
|
|
420
|
-
#
|
|
421
|
-
#
|
|
422
|
-
#
|
|
487
|
+
# Consult the head only when the tail has no occurrence of that field.
|
|
488
|
+
# Normalize blanks AFTER choosing the latest occurrence: an explicit
|
|
489
|
+
# clearing entry must not resurrect an older title from the head.
|
|
423
490
|
# Summary-chain fields use the top-level-verified scan: a raw byte scan
|
|
424
491
|
# also matches these keys nested inside tool_use inputs, reporting tool
|
|
425
492
|
# arguments as the session title/summary (and diverging from the store
|
|
426
493
|
# fold, which reads top-level keys only).
|
|
427
|
-
custom_title = presence(extract_top_level_string_field(tail, 'customTitle', last: true)
|
|
428
|
-
|
|
429
|
-
presence(extract_top_level_string_field(tail, 'aiTitle', last: true)
|
|
430
|
-
|
|
494
|
+
custom_title = presence(extract_top_level_string_field(tail, 'customTitle', last: true) ||
|
|
495
|
+
extract_top_level_string_field(head, 'customTitle', last: true)) ||
|
|
496
|
+
presence(extract_top_level_string_field(tail, 'aiTitle', last: true) ||
|
|
497
|
+
extract_top_level_string_field(head, 'aiTitle', last: true))
|
|
431
498
|
first_prompt = extract_first_prompt_from_head(head)
|
|
432
499
|
# lastPrompt tail entry shows what the user was most recently doing.
|
|
433
500
|
summary = custom_title ||
|
|
@@ -439,8 +506,7 @@ module ClaudeAgentSDK
|
|
|
439
506
|
# Scope tag extraction to {"type":"tag"} lines — a bare tail scan for
|
|
440
507
|
# "tag" would match tool_use inputs (git tag, Docker tags, etc.).
|
|
441
508
|
tag_line = tail.lines.reverse.find { |ln| ln.start_with?('{"type":"tag"') }
|
|
442
|
-
tag_value = tag_line ? extract_json_string_field(tag_line, 'tag', last: true) : nil
|
|
443
|
-
tag_value = nil if tag_value && tag_value.empty?
|
|
509
|
+
tag_value = presence(tag_line ? extract_json_string_field(tag_line, 'tag', last: true) : nil)
|
|
444
510
|
|
|
445
511
|
# created_at from the first ISO timestamp found in the head (epoch ms).
|
|
446
512
|
# More reliable than stat().birthtime which is unsupported on some
|
|
@@ -458,9 +524,11 @@ module ClaudeAgentSDK
|
|
|
458
524
|
file_size: stat.size,
|
|
459
525
|
custom_title: custom_title,
|
|
460
526
|
first_prompt: first_prompt,
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
527
|
+
# presence: blank metadata reads as absent, exactly as the store path
|
|
528
|
+
# (SessionSummary.summary_entry_to_sdk_info) reports it.
|
|
529
|
+
git_branch: presence(extract_json_string_field(tail, 'gitBranch', last: true) ||
|
|
530
|
+
extract_json_string_field(head, 'gitBranch', last: false)),
|
|
531
|
+
cwd: presence(extract_json_string_field(head, 'cwd', last: false)) || project_path,
|
|
464
532
|
tag: tag_value,
|
|
465
533
|
created_at: created_at
|
|
466
534
|
)
|
|
@@ -509,11 +577,12 @@ module ClaudeAgentSDK
|
|
|
509
577
|
list_all_sessions
|
|
510
578
|
end
|
|
511
579
|
|
|
512
|
-
# Sort by last_modified descending
|
|
580
|
+
# Sort by last_modified descending (ties by session_id, identically to
|
|
581
|
+
# the store path), then apply offset and limit.
|
|
513
582
|
# [limit, 0].max: limit <= 0 yields [] across the whole read-API family
|
|
514
583
|
# (a bare first(-1) would raise ArgumentError here but silently clamp on
|
|
515
584
|
# the store paths).
|
|
516
|
-
sessions.sort_by! { |s|
|
|
585
|
+
sessions.sort_by! { |s| listing_sort_key(s.last_modified, s.session_id) }
|
|
517
586
|
sessions = sessions[offset..] || [] if offset.positive?
|
|
518
587
|
sessions = sessions.first([limit, 0].max) if limit
|
|
519
588
|
sessions
|
|
@@ -526,7 +595,7 @@ module ClaudeAgentSDK
|
|
|
526
595
|
# project directories are searched.
|
|
527
596
|
# @return [SDKSessionInfo, nil] Session info, or nil if not found / sidechain / no summary
|
|
528
597
|
def get_session_info(session_id:, directory: nil)
|
|
529
|
-
return nil unless
|
|
598
|
+
return nil unless valid_session_id?(session_id)
|
|
530
599
|
|
|
531
600
|
file_name = "#{session_id}.jsonl"
|
|
532
601
|
return get_session_info_for_directory(file_name, directory) if directory
|
|
@@ -552,7 +621,7 @@ module ClaudeAgentSDK
|
|
|
552
621
|
# @param offset [Integer] Number of messages to skip
|
|
553
622
|
# @return [Array<SessionMessage>] Ordered messages from the session
|
|
554
623
|
def get_session_messages(session_id:, directory: nil, limit: nil, offset: 0)
|
|
555
|
-
return [] unless
|
|
624
|
+
return [] unless valid_session_id?(session_id)
|
|
556
625
|
|
|
557
626
|
offset ||= 0
|
|
558
627
|
|
|
@@ -588,7 +657,7 @@ module ClaudeAgentSDK
|
|
|
588
657
|
# scopes to that project + its worktrees; nil searches all projects)
|
|
589
658
|
# @return [Array<String>] Subagent IDs
|
|
590
659
|
def list_subagents(session_id:, directory: nil)
|
|
591
|
-
return [] unless
|
|
660
|
+
return [] unless valid_session_id?(session_id)
|
|
592
661
|
|
|
593
662
|
subagents_dir = resolve_subagents_dir(session_id, directory)
|
|
594
663
|
return [] if subagents_dir.nil?
|
|
@@ -601,8 +670,7 @@ module ClaudeAgentSDK
|
|
|
601
670
|
# reader. This is historical metadata, not a live status query.
|
|
602
671
|
# @return [Hash{String => Object}, nil] Original CLI fields, or nil if unavailable
|
|
603
672
|
def get_subagent_metadata(session_id:, agent_id:, directory: nil)
|
|
604
|
-
return nil unless session_id
|
|
605
|
-
return nil if agent_id.nil? || agent_id.empty?
|
|
673
|
+
return nil unless valid_session_id?(session_id) && valid_agent_id?(agent_id)
|
|
606
674
|
|
|
607
675
|
subagents_dir = resolve_subagents_dir(session_id, directory)
|
|
608
676
|
return nil if subagents_dir.nil?
|
|
@@ -625,8 +693,7 @@ module ClaudeAgentSDK
|
|
|
625
693
|
# @param offset [Integer] Number of messages to skip
|
|
626
694
|
# @return [Array<SessionMessage>] Ordered messages from the subagent
|
|
627
695
|
def get_subagent_messages(session_id:, agent_id:, directory: nil, limit: nil, offset: 0)
|
|
628
|
-
return [] unless session_id
|
|
629
|
-
return [] if agent_id.nil? || agent_id.empty?
|
|
696
|
+
return [] unless valid_session_id?(session_id) && valid_agent_id?(agent_id)
|
|
630
697
|
|
|
631
698
|
subagents_dir = resolve_subagents_dir(session_id, directory)
|
|
632
699
|
return [] if subagents_dir.nil?
|
|
@@ -723,7 +790,7 @@ module ClaudeAgentSDK
|
|
|
723
790
|
|
|
724
791
|
{ mtime: entry['mtime'] || 0, session_id: sid, info: nil }
|
|
725
792
|
end
|
|
726
|
-
slots.sort_by! { |slot|
|
|
793
|
+
slots.sort_by! { |slot| listing_sort_key(slot[:mtime], slot[:session_id]) }
|
|
727
794
|
paginate_resolving_gaps(session_store, project_key, project_path, slots, limit, offset)
|
|
728
795
|
end
|
|
729
796
|
|
|
@@ -731,7 +798,7 @@ module ClaudeAgentSDK
|
|
|
731
798
|
# counterpart to get_session_info. Returns nil for an invalid UUID, an
|
|
732
799
|
# unknown session, a sidechain session, or one with no extractable summary.
|
|
733
800
|
def get_session_info_from_store(session_store:, session_id:, directory: nil)
|
|
734
|
-
return nil unless
|
|
801
|
+
return nil unless valid_session_id?(session_id)
|
|
735
802
|
|
|
736
803
|
project_path = canonicalize_path(directory.nil? ? '.' : directory.to_s)
|
|
737
804
|
entries = session_store.load('project_key' => sanitize_path(project_path), 'session_id' => session_id)
|
|
@@ -743,7 +810,7 @@ module ClaudeAgentSDK
|
|
|
743
810
|
# Read a session's conversation messages from a SessionStore. Store-backed
|
|
744
811
|
# counterpart to get_session_messages.
|
|
745
812
|
def get_session_messages_from_store(session_store:, session_id:, directory: nil, limit: nil, offset: 0)
|
|
746
|
-
return [] unless
|
|
813
|
+
return [] unless valid_session_id?(session_id)
|
|
747
814
|
|
|
748
815
|
offset ||= 0
|
|
749
816
|
entries = session_store.load('project_key' => project_key_for_directory(directory), 'session_id' => session_id)
|
|
@@ -755,7 +822,7 @@ module ClaudeAgentSDK
|
|
|
755
822
|
# List subagent IDs for a session from a SessionStore. Requires the store to
|
|
756
823
|
# implement list_subkeys.
|
|
757
824
|
def list_subagents_from_store(session_store:, session_id:, directory: nil)
|
|
758
|
-
return [] unless
|
|
825
|
+
return [] unless valid_session_id?(session_id)
|
|
759
826
|
|
|
760
827
|
unless SessionStore.implements?(session_store, :list_subkeys)
|
|
761
828
|
raise ArgumentError,
|
|
@@ -785,8 +852,7 @@ module ClaudeAgentSDK
|
|
|
785
852
|
# remain unchanged. Adapter failures propagate, like other store readers.
|
|
786
853
|
# @return [Hash{String => Object}, nil]
|
|
787
854
|
def get_subagent_metadata_from_store(session_store:, session_id:, agent_id:, directory: nil)
|
|
788
|
-
return nil unless session_id
|
|
789
|
-
return nil if agent_id.nil? || agent_id.empty?
|
|
855
|
+
return nil unless valid_session_id?(session_id) && valid_agent_id?(agent_id)
|
|
790
856
|
|
|
791
857
|
project_key = project_key_for_directory(directory)
|
|
792
858
|
subpath = resolve_subagent_subpath(session_store, project_key, session_id, agent_id)
|
|
@@ -802,8 +868,7 @@ module ClaudeAgentSDK
|
|
|
802
868
|
# subagents/workflows/<runId>/agent-<id>; scans subkeys to resolve the path
|
|
803
869
|
# when the store implements list_subkeys, else tries the direct path.
|
|
804
870
|
def get_subagent_messages_from_store(session_store:, session_id:, agent_id:, directory: nil, limit: nil, offset: 0)
|
|
805
|
-
return [] unless session_id
|
|
806
|
-
return [] if agent_id.nil? || agent_id.empty?
|
|
871
|
+
return [] unless valid_session_id?(session_id) && valid_agent_id?(agent_id)
|
|
807
872
|
|
|
808
873
|
project_key = project_key_for_directory(directory)
|
|
809
874
|
subpath = resolve_subagent_subpath(session_store, project_key, session_id, agent_id)
|
|
@@ -834,7 +899,7 @@ module ClaudeAgentSDK
|
|
|
834
899
|
# @raise [Errno::ENOENT] if the session JSONL cannot be found
|
|
835
900
|
def import_session_to_store(session_id:, session_store:, directory: nil, include_subagents: true,
|
|
836
901
|
batch_size: TranscriptMirrorBatcher::MAX_PENDING_ENTRIES)
|
|
837
|
-
raise ArgumentError, "Invalid session_id: #{session_id}" unless
|
|
902
|
+
raise ArgumentError, "Invalid session_id: #{session_id}" unless valid_session_id?(session_id)
|
|
838
903
|
|
|
839
904
|
resolved = find_session_file(session_id, directory)
|
|
840
905
|
raise Errno::ENOENT, "Session #{session_id} not found" if resolved.nil? || !File.exist?(resolved)
|
|
@@ -884,12 +949,14 @@ module ClaudeAgentSDK
|
|
|
884
949
|
s_mtime = summary['mtime'] || 0
|
|
885
950
|
if has_list_sessions
|
|
886
951
|
known = known_mtimes[sid]
|
|
887
|
-
# known.nil?: no longer listed (drop). s_mtime < known: stale sidecar
|
|
888
|
-
|
|
952
|
+
# known.nil?: no longer listed (drop). s_mtime < known: stale sidecar
|
|
953
|
+
# (re-fold). Coerced: the sidecar and the listing may report the
|
|
954
|
+
# same clock in different shapes (epoch Integer vs ISO String).
|
|
955
|
+
next if known.nil? || sortable_mtime(s_mtime) < sortable_mtime(known)
|
|
889
956
|
end
|
|
890
957
|
fresh[sid] = true
|
|
891
958
|
info = SessionSummary.summary_entry_to_sdk_info(summary, project_path)
|
|
892
|
-
slots << { mtime: s_mtime, info: info } unless info.nil?
|
|
959
|
+
slots << { mtime: s_mtime, session_id: sid, info: info } unless info.nil?
|
|
893
960
|
end
|
|
894
961
|
listing.each do |e|
|
|
895
962
|
next if fresh[e['session_id']]
|
|
@@ -897,7 +964,7 @@ module ClaudeAgentSDK
|
|
|
897
964
|
slots << { mtime: e['mtime'] || 0, session_id: e['session_id'], info: nil }
|
|
898
965
|
end
|
|
899
966
|
|
|
900
|
-
slots.sort_by! { |slot|
|
|
967
|
+
slots.sort_by! { |slot| listing_sort_key(slot[:mtime], slot[:session_id]) }
|
|
901
968
|
paginate_resolving_gaps(store, project_key, project_path, slots, limit, offset)
|
|
902
969
|
end
|
|
903
970
|
|
|
@@ -970,7 +1037,7 @@ module ClaudeAgentSDK
|
|
|
970
1037
|
end
|
|
971
1038
|
|
|
972
1039
|
def apply_sort_limit_offset(results, limit, offset)
|
|
973
|
-
results = results.sort_by { |s|
|
|
1040
|
+
results = results.sort_by { |s| listing_sort_key(s.last_modified, s.session_id) }
|
|
974
1041
|
results = results[offset..] || [] if offset.positive?
|
|
975
1042
|
# A non-nil limit caps the result. limit <= 0 yields [] (matching the disk
|
|
976
1043
|
# readers' `first(limit) if limit` and entries_to_messages), and the
|
|
@@ -1481,7 +1548,8 @@ module ClaudeAgentSDK
|
|
|
1481
1548
|
:find_session_file, :stat_candidate, :resolve_subagents_dir,
|
|
1482
1549
|
:collect_agent_files, :parse_jsonl_entries,
|
|
1483
1550
|
:build_conversation_chain, :walk_to_leaf, :walk_to_root,
|
|
1484
|
-
:filter_visible_messages, :read_head_tail, :build_session_info, :
|
|
1551
|
+
:filter_visible_messages, :read_head_tail, :build_session_info, :user_entry_texts,
|
|
1552
|
+
:valid_session_id?, :valid_agent_id?, :listing_sort_key, :sidechain_head?,
|
|
1485
1553
|
:list_sessions_via_summaries, :paginate_resolving_gaps, :resolve_gap_slot,
|
|
1486
1554
|
:derive_info_from_entries, :mtime_from_entries, :apply_sort_limit_offset,
|
|
1487
1555
|
:filter_transcript_entries, :entries_to_messages,
|
|
@@ -55,14 +55,6 @@ module ClaudeAgentSDK
|
|
|
55
55
|
# end
|
|
56
56
|
def self.from_block(session_id: 'default', &block)
|
|
57
57
|
Enumerator.new do |yielder|
|
|
58
|
-
collector = Object.new
|
|
59
|
-
def collector.yield(content)
|
|
60
|
-
@content = content
|
|
61
|
-
end
|
|
62
|
-
def collector.content
|
|
63
|
-
@content
|
|
64
|
-
end
|
|
65
|
-
|
|
66
58
|
inner_enum = Enumerator.new(&block)
|
|
67
59
|
inner_enum.each do |content|
|
|
68
60
|
yielder << user_message(content, session_id: session_id)
|