rubik 0.4.2 → 0.6.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/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +125 -1
- data/lib/rubik/ast/cache_store.rb +33 -0
- data/lib/rubik/ast/graph.rb +2 -1
- data/lib/rubik/benchmark/runner.rb +14 -0
- data/lib/rubik/benchmark/vakanuvis_adoption.rb +439 -0
- data/lib/rubik/config/defaults.rb +7 -1
- data/lib/rubik/log.rb +4 -3
- data/lib/rubik/orchestrator/budget_manager.rb +2 -0
- data/lib/rubik/orchestrator/loop.rb +1 -0
- data/lib/rubik/orchestrator/shell_deps.rb +4 -0
- data/lib/rubik/orchestrator/step_executor.rb +3 -3
- data/lib/rubik/orchestrator/subagent/tool_dispatch.rb +31 -11
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +2 -0
- data/lib/rubik/prompt/doctrine.rb +10 -15
- data/lib/rubik/prompt/emergency_compactor.rb +7 -3
- data/lib/rubik/prompt/tool_teachings.rb +29 -35
- data/lib/rubik/repl/commands/agent_context.rb +1 -0
- data/lib/rubik/repl/commands/agent_execution.rb +7 -0
- data/lib/rubik/repl/commands/system.rb +2 -0
- data/lib/rubik/repl/input_execution.rb +5 -1
- data/lib/rubik/repl/turn_recorder.rb +3 -0
- data/lib/rubik/security/redactor.rb +13 -9
- data/lib/rubik/storage/vakanuvis/annal_writer.rb +51 -4
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +14 -2
- data/lib/rubik/storage/vakanuvis/gate.rb +23 -15
- data/lib/rubik/storage/vakanuvis/harness.rb +149 -121
- data/lib/rubik/storage/vakanuvis/job.rb +25 -0
- data/lib/rubik/storage/vakanuvis/orientation.rb +277 -0
- data/lib/rubik/storage/vakanuvis/service.rb +5 -1
- data/lib/rubik/storage/vakanuvis.rb +20 -1
- data/lib/rubik/tools/builtins/vakanuvis.rb +13 -15
- data/lib/rubik/tools/truncator.rb +53 -0
- data/lib/rubik/version.rb +1 -1
- metadata +4 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../layout"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Storage
|
|
7
|
+
module Vakanuvis
|
|
8
|
+
# Soğuk-başlangıç ve sıkıştırma sonrası yönelim enjeksiyonu (issue #340):
|
|
9
|
+
# Sistem istemi (system prompt) hiç şişirilmeden, oturumun ilk model
|
|
10
|
+
# çağrısından önce ve her bağlam sıkıştırması / budaması tamamlandığında
|
|
11
|
+
# kök konuşma geçmişine kısa, deterministik bir `role: "user"` yönelim
|
|
12
|
+
# iletisi eklenir. Kart yoksa (N == 0), kronikçi kapalıysa veya
|
|
13
|
+
# `subagents.vakanuvis.orientation == false` ise hiçbir şey enjekte
|
|
14
|
+
# edilmez. Kanca `ctx` içine tersiyle (`on_dispose`) kaydedilir (§1A).
|
|
15
|
+
module Orientation
|
|
16
|
+
DEFAULT_MAX_TOPICS = 10
|
|
17
|
+
HOOK_KEY = :vakanuvis_orientation_hook
|
|
18
|
+
TAG_OPEN = "<vakanuvis_orientation>"
|
|
19
|
+
TAG_CLOSE = "</vakanuvis_orientation>"
|
|
20
|
+
DEPTH_HINT = "Ask `vakanuvis(action: 'recall', query: '<2-3 terms>')` for depth."
|
|
21
|
+
|
|
22
|
+
# Oturum-içi durum ve terslenebilir kanca taşıyıcısı.
|
|
23
|
+
class Hook
|
|
24
|
+
attr_reader :harness
|
|
25
|
+
|
|
26
|
+
def initialize(harness, config: nil)
|
|
27
|
+
@harness = harness
|
|
28
|
+
@config = config
|
|
29
|
+
@session_oriented = false
|
|
30
|
+
@disposed = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dispose!
|
|
34
|
+
@disposed = true
|
|
35
|
+
@session_oriented = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def disposed?
|
|
39
|
+
@disposed
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset_session!
|
|
43
|
+
@session_oriented = false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def enabled?(override_config = nil)
|
|
47
|
+
return false if @disposed || @harness.nil? || @harness.stopped?
|
|
48
|
+
|
|
49
|
+
cfg = override_config || @config || @harness.config
|
|
50
|
+
return false unless Service.enabled?(cfg)
|
|
51
|
+
|
|
52
|
+
Layout.config_get(cfg, "subagents.vakanuvis.orientation", true) != false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def max_topics(override_config = nil)
|
|
56
|
+
cfg = override_config || @config || @harness.config
|
|
57
|
+
raw = Layout.config_get(cfg, "subagents.vakanuvis.orientation_max_topics", DEFAULT_MAX_TOPICS)
|
|
58
|
+
val = raw.to_i
|
|
59
|
+
val.positive? ? val : DEFAULT_MAX_TOPICS
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_message(override_config = nil)
|
|
63
|
+
return nil unless enabled?(override_config)
|
|
64
|
+
|
|
65
|
+
excerpt = Orientation.format_excerpt(@harness, limit: max_topics(override_config))
|
|
66
|
+
return nil unless excerpt
|
|
67
|
+
|
|
68
|
+
{ role: "user", content: excerpt }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inject_session_start!(history, config: nil)
|
|
72
|
+
return nil unless history.is_a?(Array)
|
|
73
|
+
|
|
74
|
+
if history.any? { |m| Orientation.orientation_message?(m) }
|
|
75
|
+
@session_oriented = true
|
|
76
|
+
return nil
|
|
77
|
+
end
|
|
78
|
+
return nil if @session_oriented
|
|
79
|
+
return nil unless enabled?(config)
|
|
80
|
+
|
|
81
|
+
msg = build_message(config)
|
|
82
|
+
return nil unless msg
|
|
83
|
+
|
|
84
|
+
@session_oriented = true
|
|
85
|
+
Orientation.deduplicate_and_insert_start!(history, msg)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def inject_after_compaction!(history, config: nil)
|
|
89
|
+
return nil unless history.is_a?(Array)
|
|
90
|
+
|
|
91
|
+
Orientation.strip_orientation_messages!(history)
|
|
92
|
+
return nil unless enabled?(config)
|
|
93
|
+
|
|
94
|
+
msg = build_message(config)
|
|
95
|
+
return nil unless msg
|
|
96
|
+
|
|
97
|
+
@session_oriented = true
|
|
98
|
+
Orientation.insert_after_compaction!(history, msg)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
module_function
|
|
103
|
+
|
|
104
|
+
# Kancayı `ctx` (veya child sub-context) içine tersiyle kaydeder (§1A).
|
|
105
|
+
# Kronikçi devre dışı bırakıldığında veya child dispose edildiğinde
|
|
106
|
+
# kanca otomatik olarak sökülür; reload döngülerinde yetim kanca kalmaz.
|
|
107
|
+
def register_hook(ctx, harness, config: nil, inverse_ctx: nil)
|
|
108
|
+
return nil unless ctx.respond_to?(:provide) && harness
|
|
109
|
+
|
|
110
|
+
cfg = config || harness.config
|
|
111
|
+
hook = Hook.new(harness, config: cfg)
|
|
112
|
+
ctx.provide(HOOK_KEY, hook)
|
|
113
|
+
target_ctx = inverse_ctx || ctx
|
|
114
|
+
cfg_inverse = wire_config_observer(ctx, hook, cfg)
|
|
115
|
+
if target_ctx.respond_to?(:on_dispose)
|
|
116
|
+
target_ctx.on_dispose do
|
|
117
|
+
cfg_inverse&.call
|
|
118
|
+
hook.dispose!
|
|
119
|
+
ctx.unprovide(HOOK_KEY) if ctx.respond_to?(:unprovide) && ctx.get(HOOK_KEY).equal?(hook)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
hook
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def wire_config_observer(ctx, hook, cfg)
|
|
126
|
+
return nil unless cfg.respond_to?(:on_change)
|
|
127
|
+
|
|
128
|
+
cfg.on_change("subagents.vakanuvis") do |_key, _val, _old|
|
|
129
|
+
next if hook.disposed?
|
|
130
|
+
|
|
131
|
+
if hook.enabled?(cfg)
|
|
132
|
+
ctx.provide(HOOK_KEY, hook) unless ctx.get(HOOK_KEY).equal?(hook)
|
|
133
|
+
elsif ctx.respond_to?(:unprovide) && ctx.get(HOOK_KEY).equal?(hook)
|
|
134
|
+
ctx.unprovide(HOOK_KEY)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Coeffect üzerinden kancayı veya doğrudan VAKANUIS harness'ini çözer.
|
|
140
|
+
# VAKANUIS coeffect'i karşılanmamışsa nil döner.
|
|
141
|
+
def resolve_hook(context_or_manager, config: nil)
|
|
142
|
+
ctx = extract_context(context_or_manager)
|
|
143
|
+
return nil unless ctx
|
|
144
|
+
|
|
145
|
+
harness = Rubik::Coeffects.get(ctx, Rubik::Coeffects::VAKANUIS)
|
|
146
|
+
return nil unless harness.is_a?(Harness) && !harness.stopped?
|
|
147
|
+
|
|
148
|
+
cfg = config || harness.config
|
|
149
|
+
return nil unless Service.enabled?(cfg)
|
|
150
|
+
return nil if Layout.config_get(cfg, "subagents.vakanuvis.orientation", true) == false
|
|
151
|
+
|
|
152
|
+
hook = Rubik::Coeffects.get(ctx, HOOK_KEY)
|
|
153
|
+
return hook if hook.is_a?(Hook) && !hook.disposed? && hook.harness.equal?(harness)
|
|
154
|
+
|
|
155
|
+
register_hook(ctx, harness, config: cfg)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def reset_session!(context_or_manager)
|
|
159
|
+
ctx = extract_context(context_or_manager)
|
|
160
|
+
return false unless ctx
|
|
161
|
+
|
|
162
|
+
hook = Rubik::Coeffects.get(ctx, HOOK_KEY)
|
|
163
|
+
hook.reset_session! if hook.is_a?(Hook)
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def inject_session_start!(history, context_or_manager, config: nil)
|
|
168
|
+
hook = resolve_hook(context_or_manager, config: config)
|
|
169
|
+
return nil unless hook
|
|
170
|
+
|
|
171
|
+
hook.inject_session_start!(history, config: config)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def inject_after_compaction!(history, context_or_manager, config: nil)
|
|
175
|
+
strip_orientation_messages!(history) if history.is_a?(Array)
|
|
176
|
+
hook = resolve_hook(context_or_manager, config: config)
|
|
177
|
+
return nil unless hook
|
|
178
|
+
|
|
179
|
+
hook.inject_after_compaction!(history, config: config)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Aktif mem.* kartlarını [ref, row] sırasıyla listeler.
|
|
183
|
+
# Dönen çift: [lines_array, total_active_cards_count].
|
|
184
|
+
def topic_lines(store, limit: nil)
|
|
185
|
+
return [[], 0] unless store
|
|
186
|
+
|
|
187
|
+
bound = store.refs.select { |ref, _| Vocabulary.memory_ref?(ref) }
|
|
188
|
+
cards = store.list(type: "memory").select do |s|
|
|
189
|
+
s.status == Vocabulary::STATUS_ACTIVE && bound.value?(s.id)
|
|
190
|
+
end
|
|
191
|
+
return [[], 0] if cards.empty?
|
|
192
|
+
|
|
193
|
+
all_lines = cards.sort_by { |s| [bound.key(s.id), s.row] }.filter_map do |s|
|
|
194
|
+
record = store.get(type: "memory", id: s.id)
|
|
195
|
+
next unless record # list→get arası silme yarışı (review L2)
|
|
196
|
+
|
|
197
|
+
"- #{bound.key(s.id)} — #{s.title} — #{first_body_line(record)} — #{store.uri_for('memory', s.id)}"
|
|
198
|
+
end
|
|
199
|
+
total = all_lines.size
|
|
200
|
+
shown = limit ? all_lines.first(limit.to_i) : all_lines
|
|
201
|
+
[shown, total]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def first_body_line(record)
|
|
205
|
+
slice = record["body"].to_s.lines.map(&:strip).reject(&:empty?).first.to_s.slice(0, Harness::RECALL_BODY_SLICE)
|
|
206
|
+
slice.empty? ? "(no body)" : slice
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Aktif mem.* kartlarından deterministik [ref, row] sırasıyla en fazla
|
|
210
|
+
# `limit` satır üretir. Aktif kart yoksa (N == 0) nil döner.
|
|
211
|
+
def format_excerpt(harness, limit: DEFAULT_MAX_TOPICS)
|
|
212
|
+
store = harness.respond_to?(:store) ? harness.store : nil
|
|
213
|
+
lines, total = topic_lines(store, limit: limit)
|
|
214
|
+
return nil if lines.empty?
|
|
215
|
+
|
|
216
|
+
count_label = total > lines.size ? "#{lines.size} of #{total}" : lines.size.to_s
|
|
217
|
+
"#{TAG_OPEN}\nRecorded topics (#{count_label}):\n#{lines.join("\n")}\n#{DEPTH_HINT}\n#{TAG_CLOSE}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def orientation_message?(msg)
|
|
221
|
+
return false unless msg.is_a?(Hash)
|
|
222
|
+
|
|
223
|
+
role = (msg[:role] || msg["role"]).to_s
|
|
224
|
+
return false unless role == "user"
|
|
225
|
+
|
|
226
|
+
content = (msg[:content] || msg["content"]).to_s
|
|
227
|
+
content.include?(TAG_OPEN) && content.include?(DEPTH_HINT)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def strip_orientation_messages!(history)
|
|
231
|
+
return history unless history.is_a?(Array)
|
|
232
|
+
|
|
233
|
+
history.reject! { |msg| orientation_message?(msg) }
|
|
234
|
+
history
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Oturum başında: sistem mesajı varsa hemen ardına (ilk user mesajından
|
|
238
|
+
# önce), yoksa dizinin başına yerleştirir. Eski kopyaları temizler.
|
|
239
|
+
def deduplicate_and_insert_start!(history, msg)
|
|
240
|
+
strip_orientation_messages!(history)
|
|
241
|
+
idx = first_non_system_index(history)
|
|
242
|
+
history.insert(idx, msg)
|
|
243
|
+
msg
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Sıkıştırma sonrasında: eski yönelim iletilerini siler ve yeni yönelim
|
|
247
|
+
# iletisini sistem mesajlarının hemen ardına deterministik olarak ekler.
|
|
248
|
+
def insert_after_compaction!(history, msg)
|
|
249
|
+
strip_orientation_messages!(history)
|
|
250
|
+
idx = first_non_system_index(history)
|
|
251
|
+
history.insert(idx, msg)
|
|
252
|
+
msg
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def first_non_system_index(history)
|
|
256
|
+
idx = history.index do |m|
|
|
257
|
+
role = m.is_a?(Hash) ? (m[:role] || m["role"]).to_s : ""
|
|
258
|
+
role != "system"
|
|
259
|
+
end
|
|
260
|
+
idx || history.size
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def extract_context(context_or_manager)
|
|
264
|
+
return nil unless context_or_manager
|
|
265
|
+
|
|
266
|
+
if context_or_manager.respond_to?(:context)
|
|
267
|
+
context_or_manager.context
|
|
268
|
+
elsif context_or_manager.respond_to?(:runtime_manager) && context_or_manager.runtime_manager.respond_to?(:context)
|
|
269
|
+
context_or_manager.runtime_manager.context
|
|
270
|
+
else
|
|
271
|
+
context_or_manager
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -57,7 +57,11 @@ module Rubik
|
|
|
57
57
|
)
|
|
58
58
|
# Ömür (§2.7): child ctx'in cascade inverse'i worker'ı kapatır;
|
|
59
59
|
# kuyruk FIFO'su drain olur, yarım kronik kalmaz.
|
|
60
|
-
child&.on_dispose
|
|
60
|
+
child&.on_dispose do
|
|
61
|
+
harness.shutdown
|
|
62
|
+
ctx.unprovide(Rubik::Coeffects::VAKANUIS) if ctx.respond_to?(:unprovide) && ctx.get(Rubik::Coeffects::VAKANUIS).equal?(harness)
|
|
63
|
+
end
|
|
64
|
+
Orientation.register_hook(ctx, harness, config: config, inverse_ctx: child)
|
|
61
65
|
harness.start!
|
|
62
66
|
ctx.provide(Rubik::Coeffects::VAKANUIS, harness)
|
|
63
67
|
context_or_manager.reconcile_all if context_or_manager.respond_to?(:reconcile_all)
|
|
@@ -50,10 +50,16 @@ module Rubik
|
|
|
50
50
|
# gereği notice YOK — sadece dmesg/stderr düzlemi.
|
|
51
51
|
SUBSYSTEM = :storage
|
|
52
52
|
|
|
53
|
+
# Konu-adlı uyarıların SON İZİ buradır: iki havuz da patlarsa bile
|
|
54
|
+
# mesaj stderr'e düşer — kayıp iz yok (review #346 F2).
|
|
53
55
|
def log_via(dmesg, level, message)
|
|
54
56
|
dmesg.respond_to?(:log) ? dmesg.log(SUBSYSTEM, level, message) : Rubik::Log.send(level, SUBSYSTEM, message)
|
|
55
57
|
rescue StandardError
|
|
56
|
-
|
|
58
|
+
begin
|
|
59
|
+
Rubik::Log.warn(SUBSYSTEM, message)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
warn("[vakanuvis] #{message}")
|
|
62
|
+
end
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
def content_unchanged?(existing, title, body, tags, links, status)
|
|
@@ -63,6 +69,17 @@ module Rubik
|
|
|
63
69
|
existing["links"] == links.to_h { |k, v| [k.to_s, v.to_s] }.sort.to_h &&
|
|
64
70
|
existing["status"] == status.to_s
|
|
65
71
|
end
|
|
72
|
+
|
|
73
|
+
# Mükerrer otomatik-damıtım kabulü (issue #351 F9): "already queued"
|
|
74
|
+
# sessiz başarı sayılmaz — mevcut iş değişmez, bu goal kaydına
|
|
75
|
+
# bağlanmaz; iz satırı düşer. Dönen true çağrı yerindeki kısa-devre
|
|
76
|
+
# koşuluna hizmet eder (mükerrer stranded DEĞİLDİR, yeniden kuyruğa
|
|
77
|
+
# girmez — var olan iş hedefi zaten derliyor).
|
|
78
|
+
def log_duplicate_digest(dmesg, topic, goal_ordinal)
|
|
79
|
+
note = goal_ordinal ? "goal record ##{goal_ordinal} not attached; " : ""
|
|
80
|
+
log_via(dmesg, :warn, "auto-digest duplicate for '#{topic}' — #{note}existing job keeps its own context")
|
|
81
|
+
true
|
|
82
|
+
end
|
|
66
83
|
end
|
|
67
84
|
end
|
|
68
85
|
end
|
|
@@ -72,6 +89,8 @@ require_relative "vakanuvis/annal_writer"
|
|
|
72
89
|
require_relative "vakanuvis/memory_steward"
|
|
73
90
|
require_relative "vakanuvis/wiki_skeleton"
|
|
74
91
|
require_relative "vakanuvis/distill_gate"
|
|
92
|
+
require_relative "vakanuvis/job"
|
|
75
93
|
require_relative "vakanuvis/gate"
|
|
76
94
|
require_relative "vakanuvis/harness"
|
|
95
|
+
require_relative "vakanuvis/orientation"
|
|
77
96
|
require_relative "vakanuvis/service"
|
|
@@ -24,15 +24,12 @@ module Rubik
|
|
|
24
24
|
DECLARATION = Declaration.new(
|
|
25
25
|
name: "vakanuvis", aliases: ["chronicler"],
|
|
26
26
|
description: "Ask the background chronicler (compiled knowledge in rubik://storage): " \
|
|
27
|
-
"'recall' (deterministic digest over memory+wiki with source links — ask at task start " \
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"evidence layer; memory cards are the durable conclusions you want recalled by default. " \
|
|
34
|
-
"Hygiene: same topic, same session = one note — update it (or retire it with forget; " \
|
|
35
|
-
"superseded rows keep a trace).",
|
|
27
|
+
"'recall' (deterministic digest over memory+wiki with source links — ask at task start), " \
|
|
28
|
+
"'record' (persist durable knowledge as a memory card; the ONE path for memory writes), " \
|
|
29
|
+
"'forget' (supersede/purge a mem.* card or a note), 'digest' (queue an async wiki compilation; " \
|
|
30
|
+
"returns a deterministic receipt, never blocks on the LLM), 'topics' (list recorded memory topics " \
|
|
31
|
+
"— orient before the first query), 'digest_status' (is my queued digest pending, in flight, or done?). " \
|
|
32
|
+
"Notes (annal.*) are the chronicler's evidence layer; memory cards are the durable conclusions you want recalled by default.",
|
|
36
33
|
risk: :write, archetypes: [], write_grant: false,
|
|
37
34
|
maestro_blocked: false, # storage emsali: kronikçi düzlemi kökte doğrudan
|
|
38
35
|
params_schema: {
|
|
@@ -42,9 +39,11 @@ module Rubik
|
|
|
42
39
|
type: "string", enum: ["recall", "record", "forget", "digest", "topics", "digest_status"],
|
|
43
40
|
description: "Operation (default: 'recall')."
|
|
44
41
|
},
|
|
45
|
-
query: {
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
query: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "recall/digest: knowledge query. Recall is keyword-AND — use 2-3 core terms (every " \
|
|
45
|
+
"space-separated term must literally appear); retry with other-language synonyms when empty."
|
|
46
|
+
},
|
|
48
47
|
types: {
|
|
49
48
|
type: "array", items: { type: "string", enum: ["memory", "wiki", "note"] },
|
|
50
49
|
description: "recall: entity scopes to search (default: memory + wiki)."
|
|
@@ -52,9 +51,8 @@ module Rubik
|
|
|
52
51
|
topic: { type: "string", description: "record: card topic — mints ref mem.<topic-slug> (upsert by topic)." },
|
|
53
52
|
body: {
|
|
54
53
|
type: "string",
|
|
55
|
-
description: "record: card body (markdown; end it with a Keywords: line of 5-10 TR+EN synonyms
|
|
56
|
-
"
|
|
57
|
-
"from the body at write time)."
|
|
54
|
+
description: "record: card body (markdown; end it with a Keywords: line of 5-10 TR+EN synonyms if you " \
|
|
55
|
+
"can — a missing Keywords: line is auto-extracted from the body at write time)."
|
|
58
56
|
},
|
|
59
57
|
tags: {
|
|
60
58
|
type: "array", items: { type: "string" },
|
|
@@ -5,6 +5,59 @@ module Rubik
|
|
|
5
5
|
module Truncator
|
|
6
6
|
DEFAULT_MAX_CHARS = 12_000
|
|
7
7
|
DEFAULT_MAX_LINES = 300
|
|
8
|
+
# The strategy knob's default lives here (arch scan card 5, 2026-11-01):
|
|
9
|
+
# both orchestrator planes read tools.truncation_strategy with this
|
|
10
|
+
# fallback — one literal for the whole repo instead of restated copies.
|
|
11
|
+
DEFAULT_STRATEGY = :head_tail
|
|
12
|
+
# Known-strategy set (issue #349): a typo'd config value used to die
|
|
13
|
+
# silently into the head_tail `else` branch in both planes. This set
|
|
14
|
+
# is the honest frontier — resolve_strategy warns against it, and a
|
|
15
|
+
# new strategy added to #truncate must register here to be reachable.
|
|
16
|
+
STRATEGIES = [:head_tail, :head_only, :tail_only].freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Configured-value resolver for the strategy fetch sites (root
|
|
20
|
+
# StepExecutor + subagent ToolDispatch + prompt EmergencyCompactor).
|
|
21
|
+
# nil/false/empty/missing fall back to the default SILENTLY (that
|
|
22
|
+
# path is correct today); a non-empty value outside STRATEGIES warns
|
|
23
|
+
# ONCE per distinct knob+value pair, then keeps the default — the
|
|
24
|
+
# operator "tuning" truncation with a typo must not get
|
|
25
|
+
# byte-identical old behavior in silence. `key:` names the config
|
|
26
|
+
# knob in the warn so each caller's message points at its own
|
|
27
|
+
# setting, and the frontier dedups on the pair: the same typo under
|
|
28
|
+
# two different knobs must warn twice, or fixing one knob leaves
|
|
29
|
+
# the other misconfigured in silence.
|
|
30
|
+
def resolve_strategy(value, key: "tools.truncation_strategy")
|
|
31
|
+
return DEFAULT_STRATEGY if value.nil? || value == false || value.to_s.strip.empty?
|
|
32
|
+
|
|
33
|
+
sym = value.to_s.downcase.to_sym
|
|
34
|
+
return sym if STRATEGIES.include?(sym)
|
|
35
|
+
|
|
36
|
+
warn_unknown_strategy(value, key)
|
|
37
|
+
DEFAULT_STRATEGY
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# One-time-per-knob-and-value warn (UI::Theme#paint precedent):
|
|
43
|
+
# the first bad value speaks, re-occurrences (every tool result
|
|
44
|
+
# would re-resolve) stay quiet. Warn level, not debug: a config
|
|
45
|
+
# typo is operator-visible in normal operation. The value is
|
|
46
|
+
# forced to UTF-8 and scrubbed BEFORE interpolation — scrub alone
|
|
47
|
+
# is a no-op on BINARY strings — because Log.warn's rescue cannot
|
|
48
|
+
# reach a message that fails to build, and a binary-encoding
|
|
49
|
+
# config value must never turn tool results into errors.
|
|
50
|
+
def warn_unknown_strategy(value, key)
|
|
51
|
+
shown = value.to_s.dup.force_encoding(Encoding::UTF_8).scrub
|
|
52
|
+
return unless unknown_strategies.add?([key, shown])
|
|
53
|
+
|
|
54
|
+
Rubik::Log.warn(:tools, "#{key}='#{shown}' is not a known strategy (#{STRATEGIES.join('/')}) — using #{DEFAULT_STRATEGY}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def unknown_strategies
|
|
58
|
+
@unknown_strategies ||= Set.new
|
|
59
|
+
end
|
|
60
|
+
end
|
|
8
61
|
|
|
9
62
|
def self.truncate(text, max_chars: DEFAULT_MAX_CHARS, max_lines: DEFAULT_MAX_LINES, strategy: :head_tail, separator: "\n")
|
|
10
63
|
str = text.to_s
|
data/lib/rubik/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubik
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Özgür Ulukır
|
|
@@ -267,6 +267,7 @@ files:
|
|
|
267
267
|
- lib/rubik/benchmark.rb
|
|
268
268
|
- lib/rubik/benchmark/runner.rb
|
|
269
269
|
- lib/rubik/benchmark/synth_repo.rb
|
|
270
|
+
- lib/rubik/benchmark/vakanuvis_adoption.rb
|
|
270
271
|
- lib/rubik/buffer.rb
|
|
271
272
|
- lib/rubik/buffer/apply_contract.rb
|
|
272
273
|
- lib/rubik/buffer/diff.rb
|
|
@@ -720,7 +721,9 @@ files:
|
|
|
720
721
|
- lib/rubik/storage/vakanuvis/distill_gate.rb
|
|
721
722
|
- lib/rubik/storage/vakanuvis/gate.rb
|
|
722
723
|
- lib/rubik/storage/vakanuvis/harness.rb
|
|
724
|
+
- lib/rubik/storage/vakanuvis/job.rb
|
|
723
725
|
- lib/rubik/storage/vakanuvis/memory_steward.rb
|
|
726
|
+
- lib/rubik/storage/vakanuvis/orientation.rb
|
|
724
727
|
- lib/rubik/storage/vakanuvis/service.rb
|
|
725
728
|
- lib/rubik/storage/vakanuvis/vocabulary.rb
|
|
726
729
|
- lib/rubik/storage/vakanuvis/wiki_skeleton.rb
|