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
|
@@ -5,12 +5,11 @@ require "monitor"
|
|
|
5
5
|
module Rubik
|
|
6
6
|
module Storage
|
|
7
7
|
module Vakanuvis
|
|
8
|
-
# Düzlem 1 — Deterministik Çekirdek: tek worker thread + sınırlı
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# dispose'u LIFO ile kapatır (worker kill + abonelik geri alımı).
|
|
8
|
+
# Düzlem 1 — Deterministik Çekirdek: tek worker thread + sınırlı FIFO kuyruk.
|
|
9
|
+
# Olay kapıları mevcut bus abonelikleridir (subagent tamamlanma + goal
|
|
10
|
+
# bitişi — K3); NE ZAMAN, NEREYE, NASIL yazılacağı burada kural tabanlıdır.
|
|
11
|
+
# Sessizlik sözleşmesi (§4.3): WAKE_KINDS dışında kalır, notice yaymaz —
|
|
12
|
+
# sadece dmesg/stderr. Ömür: ctx dispose'u LIFO ile kapatır (worker kill + abonelik geri alımı).
|
|
14
13
|
class Harness
|
|
15
14
|
STOP = :__vakanuvis_stop__
|
|
16
15
|
MAX_QUEUE = 200
|
|
@@ -18,20 +17,21 @@ module Rubik
|
|
|
18
17
|
RECALL_FALLBACK_LIMIT = 3
|
|
19
18
|
RECALL_BODY_SLICE = 140
|
|
20
19
|
RECALL_TYPES = ["memory", "wiki"].freeze
|
|
21
|
-
# v2 §4-V4: damıtıcı kaynak disiplini — yalnız TAMAMLANMIŞ
|
|
22
|
-
#
|
|
23
|
-
# canlı depoda transcript satırları 'vakanuvis' etiketi taşır).
|
|
20
|
+
# v2 §4-V4: damıtıcı kaynak disiplini — yalnız TAMAMLANMIŞ transcript'ler,
|
|
21
|
+
# damıtıcının kendisi Hariç (özyineleme kirliliği: canlı depoda transcript satırları 'vakanuvis' etiketi taşır).
|
|
24
22
|
SOURCE_TRANSCRIPT_LIMIT = 8
|
|
25
23
|
# Literaller, depolama katmanını orchestrator require'larından
|
|
26
24
|
# uzak tutar; test ikizleri her ikisini de sabitlere pinler:
|
|
27
|
-
# Notices::COMPLETED_EVENT ve GoalState::FINISH_EVENT. TURN_EVENT
|
|
28
|
-
# REPL TurnRecorder#emit_turn_finished kapısıdır (root-turn planı
|
|
29
|
-
# 2026-10-04).
|
|
25
|
+
# Notices::COMPLETED_EVENT ve GoalState::FINISH_EVENT. TURN_EVENT,
|
|
26
|
+
# REPL TurnRecorder#emit_turn_finished kapısıdır (root-turn planı 2026-10-04).
|
|
30
27
|
SUBAGENT_EVENT = :subagent_completed
|
|
31
28
|
GOAL_EVENT = :goal_finished
|
|
32
29
|
TURN_EVENT = :turn_finished
|
|
30
|
+
# Askı kroniği (tarama kartı 2, 2026-11-01): GoalState::SUSPEND_EVENT — abonelik
|
|
31
|
+
# :goal akışına yazar; worker status=="suspended" ile ayırt eder.
|
|
32
|
+
SUSPEND_EVENT = :goal_suspended
|
|
33
33
|
|
|
34
|
-
attr_reader :store, :supervisor, :dmesg, :session_slug, :gate
|
|
34
|
+
attr_reader :store, :config, :supervisor, :dmesg, :session_slug, :gate
|
|
35
35
|
|
|
36
36
|
def initialize(store:, config: nil, event_bus: nil, supervisor: nil, dmesg: nil,
|
|
37
37
|
context: nil, session_slug: nil, max_queue: MAX_QUEUE)
|
|
@@ -47,34 +47,27 @@ module Rubik
|
|
|
47
47
|
@model = fetch_cfg("subagents.vakanuvis.model", nil)
|
|
48
48
|
@retention = fetch_cfg("subagents.vakanuvis.retention", "supersede").to_s
|
|
49
49
|
@distiller_enabled = fetch_cfg("subagents.vakanuvis.distiller", true) != false
|
|
50
|
+
@auto_digest = fetch_cfg("subagents.vakanuvis.auto_digest", true) != false
|
|
50
51
|
@root_turns = fetch_cfg("subagents.vakanuvis.root_turns", true) != false
|
|
51
52
|
@max_queue = max_queue.to_i
|
|
52
53
|
@queue = Thread::Queue.new
|
|
53
54
|
@monitor = Monitor.new
|
|
54
|
-
@seq = 0
|
|
55
|
-
@goal_ordinal = 0
|
|
56
|
-
@turn_ordinal = 0
|
|
57
|
-
@events_since_distill = 0
|
|
58
|
-
@dropped = 0
|
|
55
|
+
@seq = @goal_ordinal = @turn_ordinal = @events_since_distill = @dropped = @busy = 0
|
|
59
56
|
@stopped = false
|
|
60
|
-
@busy = 0
|
|
61
57
|
@worker = nil
|
|
58
|
+
@touched_topics = Set.new
|
|
62
59
|
# Damıtıcı doğum kapısı: kuyruk + sicil + kirli işaret Gate'de
|
|
63
60
|
# (kurucu enjeksiyonu — gate Harness'e geri dokunmaz).
|
|
64
|
-
@gate = DistillGate::Gate.new(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
@gate = DistillGate::Gate.new(
|
|
62
|
+
store: @store, supervisor: @supervisor, dmesg: @dmesg, event_bus: @event_bus, enabled: @distiller_enabled, max_steps: @max_steps,
|
|
63
|
+
model: @model, session_slug: @session_slug,
|
|
64
|
+
max_digest_jobs: fetch_cfg("subagents.vakanuvis.max_digest_jobs", DistillGate::Gate::DEFAULT_MAX_DIGEST_JOBS)
|
|
65
|
+
)
|
|
69
66
|
end
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def mint_session_slug
|
|
75
|
-
"#{Time.now.utc.strftime('%Y%m%d-%H%M%S')}-#{Process.pid}"
|
|
76
|
-
end
|
|
77
|
-
end
|
|
68
|
+
# Oturum slug'ı: tek süreçte deterministik, süreçler arasında
|
|
69
|
+
# ayrışır. Replay testleri sabit slug enjekte eder.
|
|
70
|
+
def self.mint_session_slug = "#{Time.now.utc.strftime('%Y%m%d-%H%M%S')}-#{Process.pid}"
|
|
78
71
|
|
|
79
72
|
# ---- yaşam döngüsü -------------------------------------------------
|
|
80
73
|
|
|
@@ -87,10 +80,9 @@ module Rubik
|
|
|
87
80
|
|
|
88
81
|
def stopped? = @monitor.synchronize { @stopped }
|
|
89
82
|
|
|
90
|
-
# Dispose yolu: FIFO'daki her şey (STOP dahil öncesindeki olaylar)
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
# işlenemeyen olay sayısı dmesg'e düşer — sessiz kayıp yok.
|
|
83
|
+
# Dispose yolu: FIFO'daki her şey (STOP dahil öncesindeki olaylar) işlenir,
|
|
84
|
+
# sonra thread sonlanır. Kalan olay yarımda kalmaz; thread 2 saniyede durmazsa
|
|
85
|
+
# (yavaş bir flush/spawn içinde) kill gelir ve işlenemeyen olay sayısı dmesg'e düşer — sessiz kayıp yok.
|
|
94
86
|
def shutdown
|
|
95
87
|
@monitor.synchronize do
|
|
96
88
|
return if @stopped
|
|
@@ -99,6 +91,11 @@ module Rubik
|
|
|
99
91
|
@busy += 1 # STOP işaretinin de işleniş sayılır
|
|
100
92
|
@queue.push(STOP)
|
|
101
93
|
end
|
|
94
|
+
begin
|
|
95
|
+
auto_digest_touched!(spawn: false)
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
Vakanuvis.log_via(@dmesg, :warn, "shutdown auto-digest failed (#{e.class}: #{e.message}) — worker teardown continues")
|
|
98
|
+
end
|
|
102
99
|
@worker&.join(2)
|
|
103
100
|
if @worker&.alive?
|
|
104
101
|
Vakanuvis.log_via(@dmesg, :warn, "worker did not stop in 2s — killing with #{queue_size} event(s) unprocessed")
|
|
@@ -113,8 +110,7 @@ module Rubik
|
|
|
113
110
|
!busy?
|
|
114
111
|
end
|
|
115
112
|
|
|
116
|
-
# Kuyrukta + uçuştaki olay sayısı (push'ta artar, işlenince/düşürülünce
|
|
117
|
-
# azalır) — pop ile işaretleme arasındaki yarış penceresini kapatır.
|
|
113
|
+
# Kuyrukta + uçuştaki olay sayısı (push'ta artar, işlenince/düşürülünce azalır) — pop ile işaretleme arasındaki yarış penceresini kapatır.
|
|
118
114
|
def busy? = @monitor.synchronize { @busy.positive? }
|
|
119
115
|
|
|
120
116
|
# ---- olay giriş kapıları (kworker thread'inde çağrılır) -----------
|
|
@@ -140,10 +136,9 @@ module Rubik
|
|
|
140
136
|
# ---- talep düzlemi (maestro aracı buraya delege eder; K7) ---------
|
|
141
137
|
|
|
142
138
|
# Tam deterministik, LLM'siz: Store#search_ranked BM25-lite sırası +
|
|
143
|
-
# RECALL_TYPES kısıtı + ilk RECALL_LIMIT kayıt ⇒ aynı depo ⇒ aynı
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
# Store#search'in [type,row] sözleşmesi değişmez.
|
|
139
|
+
# RECALL_TYPES kısıtı + ilk RECALL_LIMIT kayıt ⇒ aynı depo ⇒ aynı digest.
|
|
140
|
+
# type: dizisi (v2 §4-V8) transcript/note gibi arama dışı tipleri hiç parse
|
|
141
|
+
# etmez; sıralama politika kararına aittir — Store#search'in [type,row] sözleşmesi değişmez.
|
|
147
142
|
def recall(query, types: RECALL_TYPES)
|
|
148
143
|
q = query.to_s.strip
|
|
149
144
|
raise ArgumentError, "recall: 'query' is required" if q.empty?
|
|
@@ -152,19 +147,29 @@ module Rubik
|
|
|
152
147
|
allowed = RECALL_TYPES if allowed.empty?
|
|
153
148
|
hits = DistillGate.active_recall_hits(@store, query: q, type: allowed).first(RECALL_LIMIT)
|
|
154
149
|
unless hits.empty?
|
|
155
|
-
lines = hits.map { |r| recall_line(r) }
|
|
156
150
|
header = "Recalled #{hits.size} entit#{hits.size == 1 ? 'y' : 'ies'} for '#{AnnalWriter.one_line(q, 80)}':"
|
|
157
|
-
return "#{header}\n#{
|
|
151
|
+
return "#{header}\n#{hits.map { |r| recall_line(r) }.join("\n")}\nRead a full record with read_file(path: '<uri above>')."
|
|
158
152
|
end
|
|
159
153
|
|
|
160
154
|
relaxed_digest(q, allowed)
|
|
161
155
|
end
|
|
162
156
|
|
|
157
|
+
def auto_digest? = @auto_digest
|
|
158
|
+
|
|
159
|
+
def touched_topics = @monitor.synchronize { @touched_topics.to_a }
|
|
160
|
+
|
|
163
161
|
# record tool thread'inden, sweep worker'dan koşar — get-then-put
|
|
164
162
|
# dansını tek kilide alınca çift satır açma yarışı kapanır (store
|
|
165
|
-
# monitoru asla geri çağırmadığı için kilit sırası tekyönlü).
|
|
163
|
+
# monitoru asla geri çağırmadığı için kilit sırası tekyönlü). Kayıt
|
|
164
|
+
# + touched-topic izi TEK kritik bölümde: aradaki pencere, koşan
|
|
165
|
+
# bir drain'in konuyu kaçırmasın (review #346).
|
|
166
166
|
def record(topic:, body:, links: {}, tags: [])
|
|
167
|
-
|
|
167
|
+
clean_topic = topic.to_s.strip
|
|
168
|
+
@monitor.synchronize do
|
|
169
|
+
rec = MemorySteward.record(@store, topic: clean_topic, body: body, links: links, tags: tags)
|
|
170
|
+
@touched_topics << clean_topic unless clean_topic.empty?
|
|
171
|
+
rec
|
|
172
|
+
end
|
|
168
173
|
end
|
|
169
174
|
|
|
170
175
|
def forget(ref:)
|
|
@@ -190,8 +195,7 @@ module Rubik
|
|
|
190
195
|
def digest_status
|
|
191
196
|
targets = @gate.job_targets
|
|
192
197
|
if targets[:queued].empty? && targets[:in_flight].empty?
|
|
193
|
-
return "Digest queue: nothing queued, nothing in flight. "
|
|
194
|
-
"recall shows compiled pages; a submitted digest appears here until it compiles."
|
|
198
|
+
return "Digest queue: nothing queued, nothing in flight. recall shows compiled pages; a submitted digest appears here until it compiles."
|
|
195
199
|
end
|
|
196
200
|
|
|
197
201
|
"Digest queue: #{targets[:queued].size} queued (#{digest_target_summary(targets[:queued])}), " \
|
|
@@ -201,23 +205,12 @@ module Rubik
|
|
|
201
205
|
# Soğuk-başlangıç yönelimi (issue #99 P2): ilk sorgu sözcük dağı
|
|
202
206
|
# tahmin etmek zorunda kalmadan hangi konular var? Aktif ve mem.*
|
|
203
207
|
# bağlı kartlar [ref, row] sırasıyla — deterministik.
|
|
204
|
-
def topics
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
s.status == Vocabulary::STATUS_ACTIVE && bound.value?(s.id)
|
|
208
|
-
end
|
|
209
|
-
if cards.empty?
|
|
210
|
-
return "No memory topics recorded yet — teach the chronicler with " \
|
|
211
|
-
"vakanuvis(action: 'record', topic: ..., body: ...)."
|
|
212
|
-
end
|
|
208
|
+
def topics(limit: nil)
|
|
209
|
+
lines, total = Orientation.topic_lines(@store, limit: limit)
|
|
210
|
+
return "No memory topics recorded yet — teach the chronicler with vakanuvis(action: 'record', topic: ..., body: ...)." if lines.empty?
|
|
213
211
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
next unless record # list→get arası silme yarışı (review L2)
|
|
217
|
-
|
|
218
|
-
"- #{bound.key(s.id)} — #{s.title} — #{first_body_line(record)} — #{@store.uri_for('memory', s.id)}"
|
|
219
|
-
end
|
|
220
|
-
"Recorded topics (#{lines.size}):\n#{lines.join("\n")}"
|
|
212
|
+
count_label = limit && total > lines.size ? "#{lines.size} of #{total}" : lines.size.to_s
|
|
213
|
+
"Recorded topics (#{count_label}):\n#{lines.join("\n")}"
|
|
221
214
|
end
|
|
222
215
|
|
|
223
216
|
private
|
|
@@ -228,13 +221,12 @@ module Rubik
|
|
|
228
221
|
shown.join(", ")
|
|
229
222
|
end
|
|
230
223
|
|
|
231
|
-
# v2 §4-V2 + issue #99 P2: AND boşluğu tek tip körlük değil — en
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
# yönlendirmesi burada kesilir).
|
|
224
|
+
# v2 §4-V2 + issue #99 P2: AND boşluğu tek tip körlük değil — en yakın
|
|
225
|
+
# kısmi eşleşmeler dürüstçe "(fallback)" damgasıyla sunulur (gerçek
|
|
226
|
+
# eşleşme değildir), transcript izleri kapsam ipucu olarak sayılır. Ne
|
|
227
|
+
# fallback ne transcript izi varsa bugünkü dil/terim yönlendirmesi birebir
|
|
228
|
+
# döner (keyword-AND'da en sık neden TR sorgu ↔ EN gövde uyuşmazlığıdır;
|
|
229
|
+
# yanlış "hemen yeniden kaydet" yönlendirmesi burada kesilir).
|
|
238
230
|
def relaxed_digest(query, allowed)
|
|
239
231
|
q = query.to_s.strip
|
|
240
232
|
# Süpersede edilmiş sayılar "en yakın kısmi eşleşme" olarak
|
|
@@ -244,9 +236,8 @@ module Rubik
|
|
|
244
236
|
.first(RECALL_FALLBACK_LIMIT)
|
|
245
237
|
transcripts = @store.search(query: q, type: "transcript")
|
|
246
238
|
if relaxed.empty? && transcripts.empty?
|
|
247
|
-
return "No chronicled knowledge matches '#{AnnalWriter.one_line(q, 80)}'. Recall is keyword-AND: retry with " \
|
|
248
|
-
"
|
|
249
|
-
"missing, record it (vakanuvis action 'record') or queue a digest."
|
|
239
|
+
return "No chronicled knowledge matches '#{AnnalWriter.one_line(q, 80)}'. Recall is keyword-AND: retry with fewer or other-language " \
|
|
240
|
+
"terms first (e.g. the TR/EN Keywords: lines). If the knowledge is genuinely missing, record it (vakanuvis action 'record') or queue a digest."
|
|
250
241
|
end
|
|
251
242
|
|
|
252
243
|
parts = []
|
|
@@ -272,23 +263,17 @@ module Rubik
|
|
|
272
263
|
slice.empty? ? "(no body)" : slice
|
|
273
264
|
end
|
|
274
265
|
|
|
275
|
-
def recall_line(record)
|
|
276
|
-
"- #{record['title']} [#{record['id']}] #{first_body_line(record)} — #{@store.uri_for(record['type'], record['id'])}"
|
|
277
|
-
end
|
|
266
|
+
def recall_line(record) = "- #{record['title']} [#{record['id']}] #{first_body_line(record)} — #{@store.uri_for(record['type'], record['id'])}"
|
|
278
267
|
|
|
268
|
+
# Abonelik geri alımı dispatcher'a aittir (§1A: subscribe'a verilen context'e
|
|
269
|
+
# unregister inverse'i kendisi bağlar); TURN aboneliği R7 knob'unun arkasındadır.
|
|
279
270
|
def subscribe_events
|
|
280
271
|
return unless @event_bus.respond_to?(:subscribe)
|
|
281
272
|
|
|
282
|
-
# Aboneliğin geri alımı ctx.on_dispose ile aynı nefeste (§1A):
|
|
283
|
-
# dispatcher subscribe'a verilen context'e unregister inverse'i
|
|
284
|
-
# kendisi bağlar.
|
|
285
273
|
@event_bus.subscribe(SUBAGENT_EVENT, context: @context) { |p| enqueue_event(:subagent, p) }
|
|
286
274
|
@event_bus.subscribe(GOAL_EVENT, context: @context) { |p| enqueue_event(:goal, p) }
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
# Kök-turn kapısı knob arkasında (R7): kapalıyken abonelik hiç
|
|
290
|
-
# kurulmaz, :turn_finished olayı kronikçiye akmaz.
|
|
291
|
-
@event_bus.subscribe(TURN_EVENT, context: @context) { |p| enqueue_event(:turn, p) }
|
|
275
|
+
@event_bus.subscribe(SUSPEND_EVENT, context: @context) { |p| enqueue_event(:goal, p) }
|
|
276
|
+
@event_bus.subscribe(TURN_EVENT, context: @context) { |p| enqueue_event(:turn, p) } if @root_turns
|
|
292
277
|
end
|
|
293
278
|
|
|
294
279
|
# Taşma düşürmesi asla STOP işaretini yutamaz (yutsa worker hiç
|
|
@@ -314,13 +299,19 @@ module Rubik
|
|
|
314
299
|
break if kind == STOP
|
|
315
300
|
|
|
316
301
|
handle_event(kind, payload)
|
|
302
|
+
rescue StandardError => e
|
|
303
|
+
# Last-ditch net: a raise escaping handle_event's own rescue
|
|
304
|
+
# would kill the worker with queued events unconsumed — and
|
|
305
|
+
# shutdown's alive?-check stays blind to that loss.
|
|
306
|
+
Vakanuvis.log_via(@dmesg, :warn, "chronicler worker event dispatch failed (#{e.class}: #{e.message}) — worker continues")
|
|
317
307
|
ensure
|
|
318
308
|
@monitor.synchronize { @busy -= 1 }
|
|
319
309
|
end
|
|
320
310
|
end
|
|
321
311
|
|
|
322
312
|
# TranscriptIndexer disiplini: kronikçinin kendi hatası akışı
|
|
323
|
-
# asla bozmaz — uyarı düşer, sonraki olay işlenir.
|
|
313
|
+
# asla bozmaz — uyarı düşer, sonraki olay işlenir. Kimlik eki
|
|
314
|
+
# (F7) AnnalWriter.event_context'te.
|
|
324
315
|
def handle_event(kind, payload)
|
|
325
316
|
case kind
|
|
326
317
|
when :subagent then handle_subagent(payload)
|
|
@@ -328,27 +319,27 @@ module Rubik
|
|
|
328
319
|
when :turn then handle_turn(payload)
|
|
329
320
|
end
|
|
330
321
|
rescue StandardError => e
|
|
331
|
-
|
|
322
|
+
# İlk backtrace karesi (event_loop.rb kuralı): depo hatasını
|
|
323
|
+
# annal/digest hatasından ayırt eder.
|
|
324
|
+
Vakanuvis.log_via(@dmesg, :warn, "event handling failed (#{kind}#{AnnalWriter.event_context(kind, payload)}): #{e.class}: #{e.message} @ #{(e.backtrace || []).first}")
|
|
332
325
|
end
|
|
333
326
|
|
|
334
327
|
def handle_subagent(payload)
|
|
335
328
|
# Önce kronik, sonra hasat; iki düzlemin hataları birbirini
|
|
336
329
|
# engellemez. Annal yazılamasa da digest tamamlanır/geri kuyruğa alınır.
|
|
337
330
|
seq = next_seq
|
|
338
|
-
line = AnnalWriter.subagent_line(seq, payload)
|
|
339
331
|
begin
|
|
340
|
-
append_session([[event_key(payload),
|
|
332
|
+
append_session([[event_key(payload), AnnalWriter.subagent_line(seq, payload)]])
|
|
341
333
|
ensure
|
|
342
334
|
@gate.completion_event(payload)
|
|
343
335
|
end
|
|
344
336
|
maintain if flush_due?
|
|
345
337
|
end
|
|
346
338
|
|
|
347
|
-
# Kök-turn kroniği: ordinal session-slug süreç-başına mint
|
|
348
|
-
#
|
|
349
|
-
#
|
|
350
|
-
#
|
|
351
|
-
# maintain tetiklemez — turn sıklığı yüksek (R8).
|
|
339
|
+
# Kök-turn kroniği: ordinal session-slug süreç-başına mint edildiğinden
|
|
340
|
+
# harness-lokal sayaç yeterli (yeni süreç = yeni session annal; goal
|
|
341
|
+
# sırasındaki mağazadan-türetme burada gereksiz). Turn olayı flush
|
|
342
|
+
# sayacına sayılır ama koşulsuz maintain tetiklemez — turn sıklığı yüksek (R8).
|
|
352
343
|
def handle_turn(payload)
|
|
353
344
|
ordinal = @monitor.synchronize { @turn_ordinal += 1 }
|
|
354
345
|
seq = next_seq
|
|
@@ -357,35 +348,35 @@ module Rubik
|
|
|
357
348
|
end
|
|
358
349
|
|
|
359
350
|
def handle_goal(payload)
|
|
351
|
+
if payload[:status].to_s == "suspended" # askı kroniği: satır düşer, mint/upsert/maintain yok (kart 2)
|
|
352
|
+
seq = next_seq # anahtar hedef kimliği — sıra peek'i değil (issue #350)
|
|
353
|
+
return append_session(AnnalWriter.suspend_line(seq, payload))
|
|
354
|
+
end
|
|
360
355
|
ordinal = next_goal_ordinal
|
|
361
356
|
enriched = payload.merge(goal_ordinal: ordinal)
|
|
362
357
|
seq = next_seq
|
|
363
|
-
goal_body = [AnnalWriter.goal_header_line(enriched[:goal])] +
|
|
364
|
-
AnnalWriter.goal_lines(seq, enriched).map { |_k, l| l }
|
|
358
|
+
goal_body = [AnnalWriter.goal_header_line(enriched[:goal])] + AnnalWriter.goal_lines(seq, enriched).map { |_k, l| l }
|
|
365
359
|
Vakanuvis.upsert(@store, type: "note", ref: Vocabulary.goal_ref(ordinal),
|
|
366
|
-
title: AnnalWriter.goal_title(ordinal, enriched[:goal]),
|
|
367
|
-
|
|
368
|
-
links: { "session" => Vocabulary.session_ref(@session_slug) })
|
|
360
|
+
title: AnnalWriter.goal_title(ordinal, enriched[:goal]), body: goal_body.join("\n"),
|
|
361
|
+
tags: Vocabulary::ANNAL_TAGS + [Vocabulary::GOAL_TAG], links: { "session" => Vocabulary.session_ref(@session_slug) })
|
|
369
362
|
append_session(AnnalWriter.goal_lines(seq, enriched))
|
|
370
|
-
maintain
|
|
363
|
+
maintain(goal_end: true, goal_ordinal: ordinal)
|
|
371
364
|
end
|
|
372
365
|
|
|
373
366
|
# Goal sırası mağazadan türetilir (refs'teki en büyük annal.goal.<n>
|
|
374
|
-
# + 1): süreç yeniden başlasa bile aynı depoda numara çakışmaz
|
|
375
|
-
#
|
|
376
|
-
def next_goal_ordinal
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
end
|
|
367
|
+
# + 1): süreç yeniden başlasa bile aynı depoda numara çakışmaz.
|
|
368
|
+
# Askı artık peek ETMEZ (issue #350) — sıra yalnız bitişte mint edilir.
|
|
369
|
+
def next_goal_ordinal = @goal_ordinal = peek_goal_ordinal
|
|
370
|
+
|
|
371
|
+
def peek_goal_ordinal
|
|
372
|
+
@monitor.synchronize { [@goal_ordinal, @store.refs.keys.filter_map { |k| Vocabulary.goal_ordinal(k) }.max.to_i].max + 1 }
|
|
381
373
|
end
|
|
382
374
|
|
|
383
375
|
# Flush sayacı (R8): subagent ve turn olayları aynı ölçüde sayılır;
|
|
384
376
|
# flush_events eşiği aşılınca true döner ve sayaç sıfırlanır.
|
|
385
377
|
def flush_due?
|
|
386
378
|
@monitor.synchronize do
|
|
387
|
-
@events_since_distill += 1
|
|
388
|
-
crossed = @events_since_distill >= @flush_events
|
|
379
|
+
crossed = (@events_since_distill += 1) >= @flush_events
|
|
389
380
|
@events_since_distill = 0 if crossed
|
|
390
381
|
crossed
|
|
391
382
|
end
|
|
@@ -393,16 +384,59 @@ module Rubik
|
|
|
393
384
|
|
|
394
385
|
# Flush bakımı (yalnızca flush noktalarında — flush_events eşiği
|
|
395
386
|
# aşımı veya goal bitişi): kaçak memory süpürme (K9) + wiki home
|
|
396
|
-
# yeniden hesap +
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
def maintain
|
|
387
|
+
# yeniden hesap + dokunulan hafıza konularının otomatik damıtımı +
|
|
388
|
+
# damıtıcı doğum kararı. v2 §4-V5: toplu doğum yalnızca kirli işaretle;
|
|
389
|
+
# hedefli damıtım TÜM konuları kuyrukladıysa toplu doğum atlanır — kayıp/red varsa batch telafi eder.
|
|
390
|
+
def maintain(goal_end: false, goal_ordinal: nil)
|
|
400
391
|
# Sweep record() ile aynı kilide koşar (tek-yazar disiplini):
|
|
401
392
|
# get-then-put dansı çift satır açamaz.
|
|
402
393
|
swept = @monitor.synchronize { MemorySteward.sweep_rogue_memory(@store) }
|
|
403
394
|
Vakanuvis.log_via(@dmesg, :info, "steward swept #{swept} rogue memory row(s)") if swept.positive?
|
|
404
395
|
WikiSkeleton.rebuild_home(@store)
|
|
405
|
-
|
|
396
|
+
digested = auto_digest_touched!(spawn: !stopped?, goal_ordinal: goal_ordinal) if goal_end
|
|
397
|
+
@gate.flush_tick unless stopped? || digested
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Otomatik damıtım kapısı (issue #341): hedef tamamlanmasında veya
|
|
401
|
+
# kapanışta dokunulan konuları hedeflenmiş wiki derlemesi için kuyruklar;
|
|
402
|
+
# spawn=false (kapanış) zombi LLM doğumunu engeller. Konu-başı yalıtım
|
|
403
|
+
# (review #346): tek başarısız/red'li submit kalan konuları kaybettirmez —
|
|
404
|
+
# kayıplar @touched_topics'e geri alınır, bir sonraki flush noktası dener.
|
|
405
|
+
def auto_digest_touched!(spawn: true, goal_ordinal: nil)
|
|
406
|
+
return false unless @auto_digest && @distiller_enabled
|
|
407
|
+
|
|
408
|
+
spawn_allowed = spawn && !stopped?
|
|
409
|
+
topics = @monitor.synchronize { @touched_topics.to_a.tap { @touched_topics.clear } }
|
|
410
|
+
return false if topics.empty?
|
|
411
|
+
|
|
412
|
+
stranded = topics.reject do |topic|
|
|
413
|
+
_receipt, status = @gate.submit_with_status(topic, spawn: spawn_allowed, goal_ordinal: goal_ordinal)
|
|
414
|
+
next true if status == :duplicate && Vakanuvis.log_duplicate_digest(@dmesg, topic, goal_ordinal) # F9: sessiz başarı değil
|
|
415
|
+
next true unless status == :full
|
|
416
|
+
|
|
417
|
+
Vakanuvis.log_via(@dmesg, :warn, "auto-digest refused for '#{topic}' (gate capacity full) — kept for the next goal end or shutdown")
|
|
418
|
+
false
|
|
419
|
+
rescue StandardError => e
|
|
420
|
+
Vakanuvis.log_via(@dmesg, :warn, "auto-digest submit failed for '#{topic}' (#{e.class}: #{e.message})")
|
|
421
|
+
false
|
|
422
|
+
end
|
|
423
|
+
@monitor.synchronize { stranded.each { |t| @touched_topics << t } } unless stranded.empty?
|
|
424
|
+
unless spawn_allowed
|
|
425
|
+
queued = topics - stranded
|
|
426
|
+
note = "shutdown: #{queued.size} touched topic(s) queued but not spawned — they will not compile this session: #{queued.join(', ')}"
|
|
427
|
+
note += "; #{stranded.size} refused/failed topic(s) kept for the next flush point (warnings above)" unless stranded.empty?
|
|
428
|
+
Vakanuvis.log_via(@dmesg, :warn, note)
|
|
429
|
+
end
|
|
430
|
+
# Kapalı doğum: kapı :accepted dedi ama damıtıcı hiç doğamaz —
|
|
431
|
+
# uyarı yoksa topic'ler tüm oturum kuyrukta sessizce birikirdi.
|
|
432
|
+
if spawn_allowed && @gate.respond_to?(:spawn_allowed?) && !@gate.spawn_allowed?
|
|
433
|
+
queued = topics - stranded
|
|
434
|
+
unless queued.empty?
|
|
435
|
+
note = "auto-digest queued #{queued.size} touched topic(s) but no distiller can run (spawn disabled)"
|
|
436
|
+
Vakanuvis.log_via(@dmesg, :warn, "#{note} — they will not compile this session: #{queued.join(', ')}")
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
stranded.empty?
|
|
406
440
|
end
|
|
407
441
|
|
|
408
442
|
def append_session(lines)
|
|
@@ -414,18 +448,12 @@ module Rubik
|
|
|
414
448
|
@dropped = 0
|
|
415
449
|
end
|
|
416
450
|
end
|
|
417
|
-
AnnalWriter.append(@store, ref: Vocabulary.session_ref(@session_slug),
|
|
418
|
-
title: AnnalWriter.session_title(@session_slug),
|
|
419
|
-
lines: lines + overflow.to_a)
|
|
451
|
+
AnnalWriter.append(@store, ref: Vocabulary.session_ref(@session_slug), title: AnnalWriter.session_title(@session_slug), lines: lines + overflow.to_a)
|
|
420
452
|
end
|
|
421
453
|
|
|
422
454
|
def next_seq = @monitor.synchronize { @seq += 1 }
|
|
423
455
|
|
|
424
|
-
def event_key(payload)
|
|
425
|
-
sid = payload[:storage_id].to_s
|
|
426
|
-
sid = payload[:id].to_s if sid.empty?
|
|
427
|
-
"subagent.#{sid}.#{payload[:state]}"
|
|
428
|
-
end
|
|
456
|
+
def event_key(payload) = "subagent.#{payload[:storage_id].to_s.empty? ? payload[:id] : payload[:storage_id]}.#{payload[:state]}"
|
|
429
457
|
|
|
430
458
|
def fetch_cfg(key, default)
|
|
431
459
|
@config.respond_to?(:fetch) ? @config.fetch(key, default) : default
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Storage
|
|
5
|
+
module Vakanuvis
|
|
6
|
+
module DistillGate
|
|
7
|
+
# Digest iş sözleşmesinin tek evi (tarama kartı 1, 2026-11-01 — defer #9
|
|
8
|
+
# kapanışı): kuyrukta iki ayrı Hash literal mint ediliyor, tüketiciler
|
|
9
|
+
# dört anahtarı [] ile okuyor ve öksüz :goal_id hiçbir mint sitesinde
|
|
10
|
+
# yazılmıyordu. Job bu şekli tiper: Struct#[] sembol okumasını korur
|
|
11
|
+
# (mevcut okuma siteleri değişmez), keyword_init mint sitelerini
|
|
12
|
+
# sözlüğe sabitler, var olmayan anahtar imkânsızlaşır.
|
|
13
|
+
#
|
|
14
|
+
# - query: damıtılacak konu sorgusu (toplu işlerde nil)
|
|
15
|
+
# - target: hedef wiki ref (Vocabulary.wiki_ref) — nil ⇒ toplu derleme;
|
|
16
|
+
# nil-target ayırt etme sözleşmesi değişmez, batch onun açık bayrağıdır
|
|
17
|
+
# - transcript_uris: kaynak disiplinine uyan tamamlanmış transcript URI'leri
|
|
18
|
+
# - goal_ordinal: goal-end auto-digest taşıyıcısı (Q2, 2026-11-01) — brief
|
|
19
|
+
# goal kaydını (annal.goal.<n> note'u) kaynak listesine ekler;
|
|
20
|
+
# kapanış ve toplu işlerde nil, satır brief'e hiç girmez
|
|
21
|
+
Job = Struct.new(:query, :target, :transcript_uris, :batch, :goal_ordinal, keyword_init: true)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|