kairos-chain 3.42.0 → 3.42.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07f782a3ef998bf91435a2e35cde051338c6f1c2fa8f2e037fbba4427c400ccf
4
- data.tar.gz: 8cd549860f69e91fa94bf50a41121cb3b7625686f2438c2c665c83192f081f27
3
+ metadata.gz: 7d6b5abadc2727f37c441c8d0442da6470438537ad47f387e6c4021769c678dd
4
+ data.tar.gz: f5c625947d73fb41251829cf638b397308294daa4fed7ed94cae945ad6086b72
5
5
  SHA512:
6
- metadata.gz: e1ec23c12c08cff2ce324b34715d8eedd0277863a4e561b439c7161bd6e1b08eb10936c36db603ba621544e424953fe24247fe553917d3d4fcb6e844867d7f42
7
- data.tar.gz: 738519ef9bc54544c058f59661d08288b22d60904527dbfb7f99f501f89b35d1a80c095b2b5090750f9aff69d592acc46357fe4069064302db344659b68b5642
6
+ metadata.gz: 4948c9a12280c2ccc7e0083e056d0ad99d457fcb11d86761f060a891d2033924395521364b5f867ca30eec2cc806f5d539ccc22282657c31e31a0695a5270f55
7
+ data.tar.gz: 789de8f265c7d1dbbde4969643e26f3359887b3417ddd76fe5567348b41c9dc8c37b2f062f6e77d382abc49becd4a2703ac4de29cf8d2f63b62c9fef272a5db1
data/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
4
4
 
5
5
  This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [3.42.1] - 2026-07-14
8
+
9
+ ### Fixed — SectionWriter auto-chunk completeness & duplicate tail
10
+
11
+ Two follow-ups to the v3.42.0 auto-chunk on output-uncapped providers
12
+ (e.g. the `claude_code` CLI):
13
+
14
+ - **Completeness.** v3.42.0 added a per-chunk "rewrite ONLY these paragraphs"
15
+ bound to avoid a trailing duplicate. On `claude_code` that bound backfired —
16
+ the model under-produced and dropped the section tail, leaving long sections
17
+ incomplete. Removed the bound.
18
+ - **Duplicate tail (provider-agnostic).** Some providers "finish the section"
19
+ from the first chunk; the remaining passes then appended duplicate content.
20
+ `SectionWriter` now stops chunking once the assembled output already covers
21
+ ~the whole source (`section_complete_fraction`, default 0.85). Providers that
22
+ rewrite each chunk faithfully (e.g. API adapters, where `max_tokens` is
23
+ honoured) never trip this and still run every chunk. Net: a single clean,
24
+ complete section on both provider kinds. `chunks` in the result now reports
25
+ the number of passes actually run.
26
+
7
27
  ## [3.42.0] - 2026-07-14
8
28
 
9
29
  ### Fixed / Improved — agent SkillSet self-drive on long document sections
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.42.0"
2
+ VERSION = "3.42.1"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -23,20 +23,27 @@ module KairosMcp
23
23
  # paragraph boundaries and generate each chunk in its own pass, appending the
24
24
  # results. Short contexts stay a single pass (chunks == [context_text]).
25
25
  chunks = split_context(context_text, chunk_target)
26
+ source_chars = context_text.to_s.gsub(/\s+/, '').length
27
+ complete_at = (source_chars * complete_fraction).ceil
26
28
 
27
29
  total_units = 0
28
30
  total_chars = 0
31
+ chunks_written = 0
29
32
  chunks.each_with_index do |chunk, idx|
30
33
  first = idx.zero?
34
+ # Provider-agnostic de-duplication: some output-uncapped providers (e.g. the
35
+ # claude_code CLI) "finish the section" from the first chunk. Once the assembled
36
+ # output already covers ~the whole source, stop — further passes would only
37
+ # append duplicate content. Providers that rewrite each chunk faithfully never
38
+ # trip this (their per-chunk output tracks per-chunk input), so all chunks run.
39
+ break if !first && total_chars >= complete_at
40
+
41
+ # Continuation chunks must not repeat the section heading. (A stronger
42
+ # "rewrite only these paragraphs" bound was tried but backfired on the
43
+ # claude_code CLI — it then under-produced and dropped the section tail.)
31
44
  chunk_instructions =
32
- if chunks.length > 1
33
- # Bound EVERY chunk to its own paragraphs. Without this, providers that
34
- # "finish the section" (e.g. claude_code) regenerate the whole section from
35
- # the first chunk, so later chunks duplicate content. Continuation chunks
36
- # also must not repeat the heading.
37
- notes = [bounded_note(language)]
38
- notes << continuation_note(language) unless first
39
- "#{instructions}\n\n#{notes.join("\n")}"
45
+ if chunks.length > 1 && !first
46
+ "#{instructions}\n\n#{continuation_note(language)}"
40
47
  else
41
48
  instructions
42
49
  end
@@ -49,6 +56,7 @@ module KairosMcp
49
56
  write_chunk(output_file, gen['content'], first ? append_mode : true)
50
57
  total_units += count_units(gen['content'], language)
51
58
  total_chars += gen['content'].gsub(/\s+/, '').length
59
+ chunks_written += 1
52
60
  end
53
61
 
54
62
  {
@@ -60,7 +68,7 @@ module KairosMcp
60
68
  # non-whitespace char count so callers never size output from a bogus metric.
61
69
  'word_count' => total_units,
62
70
  'char_count' => total_chars,
63
- 'chunks' => chunks.length
71
+ 'chunks' => chunks_written
64
72
  }
65
73
  end
66
74
 
@@ -167,24 +175,20 @@ module KairosMcp
167
175
  end
168
176
 
169
177
  # Auto-chunk threshold. Sized so each pass is small enough that output-uncapped
170
- # providers (e.g. claude_code CLI) emit the whole chunk without stopping early —
178
+ # providers (e.g. claude_code CLI) emit a full section without stopping early —
171
179
  # empirically a larger single pass truncates, while ~3000-char passes complete.
172
- # The bounded_note keeps chunks from overlapping. Overridable via config.
180
+ # On such providers the first chunk may "finish the section", so the output can
181
+ # carry a trailing duplicate fragment; completeness is favoured over that cosmetic
182
+ # cost. API providers (with max_tokens honoured) produce a clean single section.
183
+ # Overridable via config.
173
184
  def chunk_target
174
185
  (@config['section_chunk_target_chars'] || 3000).to_i
175
186
  end
176
187
 
177
- # Appended to EVERY chunk (when chunking) so the model rewrites only the paragraphs
178
- # it was given and does not "finish the section" which would make chunks overlap.
179
- def bounded_note(language)
180
- if %w[ja zh ko].include?(language.to_s)
181
- '重要: 以下に与えた段落だけを推敲せよ。節はいくつかに分けて処理される。' \
182
- '与えられていない段落を書き足したり、節の続きをここで完成させたりしてはならない。'
183
- else
184
- 'IMPORTANT: Rewrite ONLY the paragraphs given below. The section is processed ' \
185
- 'in several passes; do NOT add paragraphs that are not in this input or ' \
186
- 'finish the rest of the section here.'
187
- end
188
+ # Fraction of the source (by non-whitespace chars) at which the assembled output is
189
+ # considered to already cover the whole section, so remaining chunks are skipped.
190
+ def complete_fraction
191
+ (@config['section_complete_fraction'] || 0.85).to_f
188
192
  end
189
193
 
190
194
  # Appended to a continuation chunk's instructions so the model does not repeat the
@@ -468,6 +468,40 @@ assert("T18g: append_mode applies only to the first chunk; later chunks append")
468
468
  content.start_with?('PRE') && content.include?("c1\n\nc2\n\nc3")
469
469
  end
470
470
 
471
+ assert("T18h: stops chunking once output already covers the whole source (no duplicate tail)") do
472
+ caller = MockCallerTool.new
473
+ calls = 0
474
+ caller.define_singleton_method(:invoke_tool) do |tool_name, arguments = {}, context: nil|
475
+ calls += 1
476
+ # First pass "finishes the section" (output >= source); a later pass would be a dup.
477
+ body = calls == 1 ? ('な' * 500) : 'DUPLICATE'
478
+ [{ text: JSON.generate({ 'status' => 'ok', 'response' => { 'content' => body }, 'snapshot' => {} }) }]
479
+ end
480
+ writer = SW.new(caller, { 'section_chunk_target_chars' => 100 })
481
+ ctx = (1..5).map { ('あ' * 80) }.join("\n\n") # ~400 non-ws source chars, would be 5 chunks
482
+ out = File.join(TMPDIR, 'earlystop.md')
483
+ r = writer.write(section_name: 's', instructions: 'i', context_text: ctx,
484
+ output_file: out, max_words: 100, language: 'ja', append_mode: false)
485
+ # chunk1 output (500) >= 0.85 * 400 = 340 → stop after the first pass
486
+ r['chunks'] == 1 && calls == 1 && !File.read(out).include?('DUPLICATE')
487
+ end
488
+
489
+ assert("T18i: faithful per-chunk provider still runs all chunks (no premature stop)") do
490
+ caller = MockCallerTool.new
491
+ calls = 0
492
+ caller.define_singleton_method(:invoke_tool) do |tool_name, arguments = {}, context: nil|
493
+ calls += 1
494
+ [{ text: JSON.generate({ 'status' => 'ok', 'response' => { 'content' => "p#{calls}" }, 'snapshot' => {} }) }]
495
+ end
496
+ writer = SW.new(caller, { 'section_chunk_target_chars' => 100 })
497
+ ctx = (1..4).map { ('あ' * 80) }.join("\n\n")
498
+ out = File.join(TMPDIR, 'faithful.md')
499
+ r = writer.write(section_name: 's', instructions: 'i', context_text: ctx,
500
+ output_file: out, max_words: 100, language: 'ja', append_mode: false)
501
+ # short per-chunk output never reaches the completeness threshold → all chunks run
502
+ r['chunks'] > 1 && calls == r['chunks'] && File.read(out).include?("p#{calls}")
503
+ end
504
+
471
505
  assert("T18d: max_tokens respects config ceiling override") do
472
506
  caller = MockCallerTool.new
473
507
  captured = {}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kairos-chain
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.42.0
4
+ version: 3.42.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaomi Hatakeyama