kward 0.69.0 → 0.69.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +2 -2
- data/lib/kward/rpc/session_tree_rows.rb +54 -13
- data/lib/kward/session_store.rb +5 -1
- data/lib/kward/session_tree_renderer.rb +54 -13
- data/lib/kward/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ac5f4222e4587d469ce5cb838e4fb0404309083a85a144378f16e75fdfa76ba
|
|
4
|
+
data.tar.gz: 325995cade98eb049a465bbfa9d12f2c7ffc9ed1fec0b14be889d2810741fe93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7fe3449248d4ae757422cd8e8113ad23b1bdb9dd6555015c64ea628911a8aa937a336b109945029b94f3f5e3fa4846b017af9455e839188ae3b2eb9ca1254cac
|
|
7
|
+
data.tar.gz: a92899f7e09d769220246923442450312b27852a801e64f273c3237594ab831b578ad72bde87f6fc6611c4b6fb5a02f4e6080bb7ecb2cd7cc3130e4804e76fc0
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to Kward will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.69.1] - 2026-06-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Fixed `/tree` session rendering to tolerate malformed cyclic tree records instead of overflowing the Ruby stack.
|
|
12
|
+
|
|
7
13
|
## [0.69.0] - 2026-06-17
|
|
8
14
|
|
|
9
15
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
kward (0.69.
|
|
4
|
+
kward (0.69.1)
|
|
5
5
|
base64
|
|
6
6
|
nokogiri
|
|
7
7
|
tiktoken_ruby
|
|
@@ -70,7 +70,7 @@ CHECKSUMS
|
|
|
70
70
|
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
|
71
71
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
|
72
72
|
erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
|
|
73
|
-
kward (0.69.
|
|
73
|
+
kward (0.69.1)
|
|
74
74
|
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
|
75
75
|
nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42
|
|
76
76
|
nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976
|
|
@@ -34,7 +34,12 @@ module Kward
|
|
|
34
34
|
multiple_roots = visible_roots.length > 1
|
|
35
35
|
result = []
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
stack = visible_roots.sort_by { |root| tree_contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index.map do |root, index|
|
|
38
|
+
[root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots]
|
|
39
|
+
end.reverse
|
|
40
|
+
|
|
41
|
+
until stack.empty?
|
|
42
|
+
node, indent, just_branched, show_connector, is_last, gutters, virtual_root_child = stack.pop
|
|
38
43
|
entry = node[:source]["entry"] || {}
|
|
39
44
|
entry_id = entry["id"].to_s
|
|
40
45
|
formatted = tree_entry_display(entry, tool_calls_by_id)
|
|
@@ -66,14 +71,10 @@ module Kward
|
|
|
66
71
|
end
|
|
67
72
|
connector_position = [display_indent - 1, 0].max
|
|
68
73
|
child_gutters = show_connector && !virtual_root_child ? gutters + [{ position: connector_position, show: !is_last }] : gutters
|
|
69
|
-
children.each_with_index do |child, index|
|
|
70
|
-
|
|
74
|
+
children.each_with_index.reverse_each do |child, index|
|
|
75
|
+
stack << [child, child_indent, multiple_children, multiple_children, index == children.length - 1, child_gutters, false]
|
|
71
76
|
end
|
|
72
77
|
end
|
|
73
|
-
|
|
74
|
-
visible_roots.sort_by { |root| tree_contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index do |root, index|
|
|
75
|
-
walk.call(root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots)
|
|
76
|
-
end
|
|
77
78
|
result
|
|
78
79
|
end
|
|
79
80
|
|
|
@@ -83,7 +84,9 @@ module Kward
|
|
|
83
84
|
by_id = tree_entries_by_id(roots)
|
|
84
85
|
ids = []
|
|
85
86
|
current = by_id[leaf_id.to_s]
|
|
86
|
-
|
|
87
|
+
seen = {}
|
|
88
|
+
while current && !seen[current["id"].to_s]
|
|
89
|
+
seen[current["id"].to_s] = true
|
|
87
90
|
ids << current["id"].to_s
|
|
88
91
|
current = by_id[current["parentId"].to_s]
|
|
89
92
|
end
|
|
@@ -93,8 +96,12 @@ module Kward
|
|
|
93
96
|
def tree_entries_by_id(roots)
|
|
94
97
|
roots.each_with_object({}) do |root, map|
|
|
95
98
|
stack = [root]
|
|
99
|
+
seen = {}
|
|
96
100
|
until stack.empty?
|
|
97
101
|
node = stack.pop
|
|
102
|
+
next if seen[node.object_id]
|
|
103
|
+
|
|
104
|
+
seen[node.object_id] = true
|
|
98
105
|
entry = node["entry"] || {}
|
|
99
106
|
map[entry["id"].to_s] = entry unless entry["id"].to_s.empty?
|
|
100
107
|
stack.concat(Array(node["children"]))
|
|
@@ -103,10 +110,29 @@ module Kward
|
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
def visible_tree_nodes(node)
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
results = {}
|
|
114
|
+
stack = [[node, false, {}]]
|
|
115
|
+
|
|
116
|
+
until stack.empty?
|
|
117
|
+
current, visited, seen = stack.pop
|
|
118
|
+
node_key = current.object_id
|
|
119
|
+
next if seen[node_key]
|
|
120
|
+
|
|
121
|
+
if visited
|
|
122
|
+
children = Array(current["children"]).flat_map { |child| results[child.object_id] || [] }
|
|
123
|
+
results[node_key] = if hidden_tree_entry?(current["entry"] || {})
|
|
124
|
+
children
|
|
125
|
+
else
|
|
126
|
+
[{ source: current, children: children }]
|
|
127
|
+
end
|
|
128
|
+
else
|
|
129
|
+
branch_seen = seen.merge(node_key => true)
|
|
130
|
+
stack << [current, true, seen]
|
|
131
|
+
Array(current["children"]).reverse_each { |child| stack << [child, false, branch_seen] unless branch_seen[child.object_id] }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
108
134
|
|
|
109
|
-
[
|
|
135
|
+
results[node.object_id] || []
|
|
110
136
|
end
|
|
111
137
|
|
|
112
138
|
def hidden_tree_entry?(entry)
|
|
@@ -126,15 +152,30 @@ module Kward
|
|
|
126
152
|
end
|
|
127
153
|
|
|
128
154
|
def tree_contains_active_path?(node, active_path)
|
|
129
|
-
|
|
130
|
-
|
|
155
|
+
stack = [node]
|
|
156
|
+
seen = {}
|
|
157
|
+
until stack.empty?
|
|
158
|
+
current = stack.pop
|
|
159
|
+
next if seen[current.object_id]
|
|
160
|
+
|
|
161
|
+
seen[current.object_id] = true
|
|
162
|
+
entry_id = (current[:source]["entry"] || {})["id"].to_s
|
|
163
|
+
return true if active_path.include?(entry_id)
|
|
164
|
+
|
|
165
|
+
stack.concat(current[:children])
|
|
166
|
+
end
|
|
167
|
+
false
|
|
131
168
|
end
|
|
132
169
|
|
|
133
170
|
def tree_tool_calls(roots)
|
|
134
171
|
roots.each_with_object({}) do |root, tool_calls_by_id|
|
|
135
172
|
stack = [root]
|
|
173
|
+
seen = {}
|
|
136
174
|
until stack.empty?
|
|
137
175
|
node = stack.pop
|
|
176
|
+
next if seen[node.object_id]
|
|
177
|
+
|
|
178
|
+
seen[node.object_id] = true
|
|
138
179
|
entry = node["entry"] || {}
|
|
139
180
|
message = entry["message"]
|
|
140
181
|
if entry["type"] == "message" && message.is_a?(Hash) && MessageAccess.role(message) == "assistant"
|
data/lib/kward/session_store.rb
CHANGED
|
@@ -592,7 +592,11 @@ module Kward
|
|
|
592
592
|
next unless node
|
|
593
593
|
|
|
594
594
|
parent = nodes[entry["parentId"].to_s]
|
|
595
|
-
parent
|
|
595
|
+
if parent && !parent.equal?(node)
|
|
596
|
+
parent["children"] << node unless parent["children"].include?(node)
|
|
597
|
+
else
|
|
598
|
+
roots << node unless roots.include?(node)
|
|
599
|
+
end
|
|
596
600
|
end
|
|
597
601
|
roots
|
|
598
602
|
end
|
|
@@ -19,7 +19,12 @@ module Kward
|
|
|
19
19
|
multiple_roots = visible_roots.length > 1
|
|
20
20
|
result = []
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
stack = visible_roots.sort_by { |root| session_tree_contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index.map do |root, index|
|
|
23
|
+
[root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots]
|
|
24
|
+
end.reverse
|
|
25
|
+
|
|
26
|
+
until stack.empty?
|
|
27
|
+
node, indent, just_branched, show_connector, is_last, gutters, virtual_root_child = stack.pop
|
|
23
28
|
entry = node[:source]["entry"] || {}
|
|
24
29
|
display_indent = multiple_roots ? [indent - 1, 0].max : indent
|
|
25
30
|
prefix = session_tree_visual_prefix(display_indent, gutters, show_connector && !virtual_root_child, is_last, !node[:children].empty?)
|
|
@@ -40,25 +45,40 @@ module Kward
|
|
|
40
45
|
connector_position = [display_indent - 1, 0].max
|
|
41
46
|
child_gutters = show_connector && !virtual_root_child ? gutters + [{ position: connector_position, show: !is_last }] : gutters
|
|
42
47
|
|
|
43
|
-
children.each_with_index do |child, index|
|
|
44
|
-
|
|
48
|
+
children.each_with_index.reverse_each do |child, index|
|
|
49
|
+
stack << [child, child_indent, multiple_children, multiple_children, index == children.length - 1, child_gutters, false]
|
|
45
50
|
end
|
|
46
51
|
end
|
|
47
52
|
|
|
48
|
-
visible_roots.sort_by { |root| session_tree_contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index do |root, index|
|
|
49
|
-
walk.call(root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
53
|
result
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
private
|
|
56
57
|
|
|
57
58
|
def visible_session_tree_nodes(node)
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
results = {}
|
|
60
|
+
stack = [[node, false, {}]]
|
|
61
|
+
|
|
62
|
+
until stack.empty?
|
|
63
|
+
current, visited, seen = stack.pop
|
|
64
|
+
node_key = current.object_id
|
|
65
|
+
next if seen[node_key]
|
|
66
|
+
|
|
67
|
+
if visited
|
|
68
|
+
children = Array(current["children"]).flat_map { |child| results[child.object_id] || [] }
|
|
69
|
+
results[node_key] = if hidden_session_tree_entry?(current["entry"] || {})
|
|
70
|
+
children
|
|
71
|
+
else
|
|
72
|
+
[{ source: current, children: children }]
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
branch_seen = seen.merge(node_key => true)
|
|
76
|
+
stack << [current, true, seen]
|
|
77
|
+
Array(current["children"]).reverse_each { |child| stack << [child, false, branch_seen] unless branch_seen[child.object_id] }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
60
80
|
|
|
61
|
-
[
|
|
81
|
+
results[node.object_id] || []
|
|
62
82
|
end
|
|
63
83
|
|
|
64
84
|
def hidden_session_tree_entry?(entry)
|
|
@@ -132,15 +152,28 @@ module Kward
|
|
|
132
152
|
end
|
|
133
153
|
|
|
134
154
|
def session_tree_contains_active_path?(node, active_path)
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
stack = [node]
|
|
156
|
+
seen = {}
|
|
157
|
+
until stack.empty?
|
|
158
|
+
current = stack.pop
|
|
159
|
+
next if seen[current.object_id]
|
|
160
|
+
|
|
161
|
+
seen[current.object_id] = true
|
|
162
|
+
entry_id = (current[:source]["entry"] || {})["id"].to_s
|
|
163
|
+
return true if active_path.include?(entry_id)
|
|
164
|
+
|
|
165
|
+
stack.concat(current[:children])
|
|
166
|
+
end
|
|
167
|
+
false
|
|
137
168
|
end
|
|
138
169
|
|
|
139
170
|
def session_tree_active_path(roots, leaf_id)
|
|
140
171
|
by_id = session_tree_entries_by_id(roots)
|
|
141
172
|
ids = []
|
|
142
173
|
entry = by_id[leaf_id.to_s]
|
|
143
|
-
|
|
174
|
+
seen = {}
|
|
175
|
+
while entry && !seen[entry["id"].to_s]
|
|
176
|
+
seen[entry["id"].to_s] = true
|
|
144
177
|
ids << entry["id"].to_s
|
|
145
178
|
entry = by_id[entry["parentId"].to_s]
|
|
146
179
|
end
|
|
@@ -150,8 +183,12 @@ module Kward
|
|
|
150
183
|
def session_tree_entries_by_id(roots)
|
|
151
184
|
roots.each_with_object({}) do |root, map|
|
|
152
185
|
stack = [root]
|
|
186
|
+
seen = {}
|
|
153
187
|
until stack.empty?
|
|
154
188
|
node = stack.pop
|
|
189
|
+
next if seen[node.object_id]
|
|
190
|
+
|
|
191
|
+
seen[node.object_id] = true
|
|
155
192
|
entry = node["entry"] || {}
|
|
156
193
|
map[entry["id"].to_s] = entry unless entry["id"].to_s.empty?
|
|
157
194
|
stack.concat(Array(node["children"]))
|
|
@@ -162,8 +199,12 @@ module Kward
|
|
|
162
199
|
def session_tree_tool_calls(roots)
|
|
163
200
|
roots.each_with_object({}) do |root, tool_calls|
|
|
164
201
|
stack = [root]
|
|
202
|
+
seen = {}
|
|
165
203
|
until stack.empty?
|
|
166
204
|
node = stack.pop
|
|
205
|
+
next if seen[node.object_id]
|
|
206
|
+
|
|
207
|
+
seen[node.object_id] = true
|
|
167
208
|
entry = node["entry"] || {}
|
|
168
209
|
message = entry["message"]
|
|
169
210
|
if entry["type"] == "message" && message.is_a?(Hash) && message_role(message) == "assistant"
|
data/lib/kward/version.rb
CHANGED