rubik 0.4.2 → 0.5.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 +48 -1
- data/lib/rubik/ast/cache_store.rb +33 -0
- data/lib/rubik/ast/graph.rb +2 -1
- data/lib/rubik/config/defaults.rb +7 -1
- 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/turn_executor/user_channel.rb +2 -0
- data/lib/rubik/prompt/doctrine.rb +10 -15
- data/lib/rubik/prompt/tool_teachings.rb +29 -35
- data/lib/rubik/repl/commands/agent_context.rb +1 -0
- data/lib/rubik/repl/commands/system.rb +2 -0
- data/lib/rubik/repl/turn_recorder.rb +3 -0
- data/lib/rubik/security/redactor.rb +13 -9
- data/lib/rubik/storage/vakanuvis/gate.rb +13 -8
- data/lib/rubik/storage/vakanuvis/harness.rb +115 -112
- data/lib/rubik/storage/vakanuvis/orientation.rb +277 -0
- data/lib/rubik/storage/vakanuvis/service.rb +5 -1
- data/lib/rubik/storage/vakanuvis.rb +8 -1
- data/lib/rubik/tools/builtins/vakanuvis.rb +13 -15
- data/lib/rubik/version.rb +1 -1
- metadata +2 -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,18 @@ 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
|
|
33
30
|
|
|
34
|
-
attr_reader :store, :supervisor, :dmesg, :session_slug, :gate
|
|
31
|
+
attr_reader :store, :config, :supervisor, :dmesg, :session_slug, :gate
|
|
35
32
|
|
|
36
33
|
def initialize(store:, config: nil, event_bus: nil, supervisor: nil, dmesg: nil,
|
|
37
34
|
context: nil, session_slug: nil, max_queue: MAX_QUEUE)
|
|
@@ -47,34 +44,27 @@ module Rubik
|
|
|
47
44
|
@model = fetch_cfg("subagents.vakanuvis.model", nil)
|
|
48
45
|
@retention = fetch_cfg("subagents.vakanuvis.retention", "supersede").to_s
|
|
49
46
|
@distiller_enabled = fetch_cfg("subagents.vakanuvis.distiller", true) != false
|
|
47
|
+
@auto_digest = fetch_cfg("subagents.vakanuvis.auto_digest", true) != false
|
|
50
48
|
@root_turns = fetch_cfg("subagents.vakanuvis.root_turns", true) != false
|
|
51
49
|
@max_queue = max_queue.to_i
|
|
52
50
|
@queue = Thread::Queue.new
|
|
53
51
|
@monitor = Monitor.new
|
|
54
|
-
@seq = 0
|
|
55
|
-
@goal_ordinal = 0
|
|
56
|
-
@turn_ordinal = 0
|
|
57
|
-
@events_since_distill = 0
|
|
58
|
-
@dropped = 0
|
|
52
|
+
@seq = @goal_ordinal = @turn_ordinal = @events_since_distill = @dropped = @busy = 0
|
|
59
53
|
@stopped = false
|
|
60
|
-
@busy = 0
|
|
61
54
|
@worker = nil
|
|
55
|
+
@touched_topics = Set.new
|
|
62
56
|
# Damıtıcı doğum kapısı: kuyruk + sicil + kirli işaret Gate'de
|
|
63
57
|
# (kurucu enjeksiyonu — gate Harness'e geri dokunmaz).
|
|
64
|
-
@gate = DistillGate::Gate.new(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
@gate = DistillGate::Gate.new(
|
|
59
|
+
store: @store, supervisor: @supervisor, dmesg: @dmesg, event_bus: @event_bus, enabled: @distiller_enabled, max_steps: @max_steps,
|
|
60
|
+
model: @model, session_slug: @session_slug,
|
|
61
|
+
max_digest_jobs: fetch_cfg("subagents.vakanuvis.max_digest_jobs", DistillGate::Gate::DEFAULT_MAX_DIGEST_JOBS)
|
|
62
|
+
)
|
|
69
63
|
end
|
|
70
64
|
|
|
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
|
|
65
|
+
# Oturum slug'ı: tek süreçte deterministik, süreçler arasında
|
|
66
|
+
# ayrışır. Replay testleri sabit slug enjekte eder.
|
|
67
|
+
def self.mint_session_slug = "#{Time.now.utc.strftime('%Y%m%d-%H%M%S')}-#{Process.pid}"
|
|
78
68
|
|
|
79
69
|
# ---- yaşam döngüsü -------------------------------------------------
|
|
80
70
|
|
|
@@ -87,10 +77,9 @@ module Rubik
|
|
|
87
77
|
|
|
88
78
|
def stopped? = @monitor.synchronize { @stopped }
|
|
89
79
|
|
|
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.
|
|
80
|
+
# Dispose yolu: FIFO'daki her şey (STOP dahil öncesindeki olaylar) işlenir,
|
|
81
|
+
# sonra thread sonlanır. Kalan olay yarımda kalmaz; thread 2 saniyede durmazsa
|
|
82
|
+
# (yavaş bir flush/spawn içinde) kill gelir ve işlenemeyen olay sayısı dmesg'e düşer — sessiz kayıp yok.
|
|
94
83
|
def shutdown
|
|
95
84
|
@monitor.synchronize do
|
|
96
85
|
return if @stopped
|
|
@@ -99,6 +88,11 @@ module Rubik
|
|
|
99
88
|
@busy += 1 # STOP işaretinin de işleniş sayılır
|
|
100
89
|
@queue.push(STOP)
|
|
101
90
|
end
|
|
91
|
+
begin
|
|
92
|
+
auto_digest_touched!(spawn: false)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
Vakanuvis.log_via(@dmesg, :warn, "shutdown auto-digest failed (#{e.class}: #{e.message}) — worker teardown continues")
|
|
95
|
+
end
|
|
102
96
|
@worker&.join(2)
|
|
103
97
|
if @worker&.alive?
|
|
104
98
|
Vakanuvis.log_via(@dmesg, :warn, "worker did not stop in 2s — killing with #{queue_size} event(s) unprocessed")
|
|
@@ -113,8 +107,7 @@ module Rubik
|
|
|
113
107
|
!busy?
|
|
114
108
|
end
|
|
115
109
|
|
|
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.
|
|
110
|
+
# 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
111
|
def busy? = @monitor.synchronize { @busy.positive? }
|
|
119
112
|
|
|
120
113
|
# ---- olay giriş kapıları (kworker thread'inde çağrılır) -----------
|
|
@@ -140,10 +133,9 @@ module Rubik
|
|
|
140
133
|
# ---- talep düzlemi (maestro aracı buraya delege eder; K7) ---------
|
|
141
134
|
|
|
142
135
|
# 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.
|
|
136
|
+
# RECALL_TYPES kısıtı + ilk RECALL_LIMIT kayıt ⇒ aynı depo ⇒ aynı digest.
|
|
137
|
+
# type: dizisi (v2 §4-V8) transcript/note gibi arama dışı tipleri hiç parse
|
|
138
|
+
# etmez; sıralama politika kararına aittir — Store#search'in [type,row] sözleşmesi değişmez.
|
|
147
139
|
def recall(query, types: RECALL_TYPES)
|
|
148
140
|
q = query.to_s.strip
|
|
149
141
|
raise ArgumentError, "recall: 'query' is required" if q.empty?
|
|
@@ -152,19 +144,29 @@ module Rubik
|
|
|
152
144
|
allowed = RECALL_TYPES if allowed.empty?
|
|
153
145
|
hits = DistillGate.active_recall_hits(@store, query: q, type: allowed).first(RECALL_LIMIT)
|
|
154
146
|
unless hits.empty?
|
|
155
|
-
lines = hits.map { |r| recall_line(r) }
|
|
156
147
|
header = "Recalled #{hits.size} entit#{hits.size == 1 ? 'y' : 'ies'} for '#{AnnalWriter.one_line(q, 80)}':"
|
|
157
|
-
return "#{header}\n#{
|
|
148
|
+
return "#{header}\n#{hits.map { |r| recall_line(r) }.join("\n")}\nRead a full record with read_file(path: '<uri above>')."
|
|
158
149
|
end
|
|
159
150
|
|
|
160
151
|
relaxed_digest(q, allowed)
|
|
161
152
|
end
|
|
162
153
|
|
|
154
|
+
def auto_digest? = @auto_digest
|
|
155
|
+
|
|
156
|
+
def touched_topics = @monitor.synchronize { @touched_topics.to_a }
|
|
157
|
+
|
|
163
158
|
# record tool thread'inden, sweep worker'dan koşar — get-then-put
|
|
164
159
|
# 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ü).
|
|
160
|
+
# monitoru asla geri çağırmadığı için kilit sırası tekyönlü). Kayıt
|
|
161
|
+
# + touched-topic izi TEK kritik bölümde: aradaki pencere, koşan
|
|
162
|
+
# bir drain'in konuyu kaçırmasın (review #346).
|
|
166
163
|
def record(topic:, body:, links: {}, tags: [])
|
|
167
|
-
|
|
164
|
+
clean_topic = topic.to_s.strip
|
|
165
|
+
@monitor.synchronize do
|
|
166
|
+
rec = MemorySteward.record(@store, topic: clean_topic, body: body, links: links, tags: tags)
|
|
167
|
+
@touched_topics << clean_topic unless clean_topic.empty?
|
|
168
|
+
rec
|
|
169
|
+
end
|
|
168
170
|
end
|
|
169
171
|
|
|
170
172
|
def forget(ref:)
|
|
@@ -190,8 +192,7 @@ module Rubik
|
|
|
190
192
|
def digest_status
|
|
191
193
|
targets = @gate.job_targets
|
|
192
194
|
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."
|
|
195
|
+
return "Digest queue: nothing queued, nothing in flight. recall shows compiled pages; a submitted digest appears here until it compiles."
|
|
195
196
|
end
|
|
196
197
|
|
|
197
198
|
"Digest queue: #{targets[:queued].size} queued (#{digest_target_summary(targets[:queued])}), " \
|
|
@@ -201,23 +202,12 @@ module Rubik
|
|
|
201
202
|
# Soğuk-başlangıç yönelimi (issue #99 P2): ilk sorgu sözcük dağı
|
|
202
203
|
# tahmin etmek zorunda kalmadan hangi konular var? Aktif ve mem.*
|
|
203
204
|
# 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
|
|
205
|
+
def topics(limit: nil)
|
|
206
|
+
lines, total = Orientation.topic_lines(@store, limit: limit)
|
|
207
|
+
return "No memory topics recorded yet — teach the chronicler with vakanuvis(action: 'record', topic: ..., body: ...)." if lines.empty?
|
|
213
208
|
|
|
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")}"
|
|
209
|
+
count_label = limit && total > lines.size ? "#{lines.size} of #{total}" : lines.size.to_s
|
|
210
|
+
"Recorded topics (#{count_label}):\n#{lines.join("\n")}"
|
|
221
211
|
end
|
|
222
212
|
|
|
223
213
|
private
|
|
@@ -228,13 +218,12 @@ module Rubik
|
|
|
228
218
|
shown.join(", ")
|
|
229
219
|
end
|
|
230
220
|
|
|
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).
|
|
221
|
+
# v2 §4-V2 + issue #99 P2: AND boşluğu tek tip körlük değil — en yakın
|
|
222
|
+
# kısmi eşleşmeler dürüstçe "(fallback)" damgasıyla sunulur (gerçek
|
|
223
|
+
# eşleşme değildir), transcript izleri kapsam ipucu olarak sayılır. Ne
|
|
224
|
+
# fallback ne transcript izi varsa bugünkü dil/terim yönlendirmesi birebir
|
|
225
|
+
# döner (keyword-AND'da en sık neden TR sorgu ↔ EN gövde uyuşmazlığıdır;
|
|
226
|
+
# yanlış "hemen yeniden kaydet" yönlendirmesi burada kesilir).
|
|
238
227
|
def relaxed_digest(query, allowed)
|
|
239
228
|
q = query.to_s.strip
|
|
240
229
|
# Süpersede edilmiş sayılar "en yakın kısmi eşleşme" olarak
|
|
@@ -244,9 +233,8 @@ module Rubik
|
|
|
244
233
|
.first(RECALL_FALLBACK_LIMIT)
|
|
245
234
|
transcripts = @store.search(query: q, type: "transcript")
|
|
246
235
|
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."
|
|
236
|
+
return "No chronicled knowledge matches '#{AnnalWriter.one_line(q, 80)}'. Recall is keyword-AND: retry with fewer or other-language " \
|
|
237
|
+
"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
238
|
end
|
|
251
239
|
|
|
252
240
|
parts = []
|
|
@@ -272,23 +260,16 @@ module Rubik
|
|
|
272
260
|
slice.empty? ? "(no body)" : slice
|
|
273
261
|
end
|
|
274
262
|
|
|
275
|
-
def recall_line(record)
|
|
276
|
-
"- #{record['title']} [#{record['id']}] #{first_body_line(record)} — #{@store.uri_for(record['type'], record['id'])}"
|
|
277
|
-
end
|
|
263
|
+
def recall_line(record) = "- #{record['title']} [#{record['id']}] #{first_body_line(record)} — #{@store.uri_for(record['type'], record['id'])}"
|
|
278
264
|
|
|
265
|
+
# Abonelik geri alımı dispatcher'a aittir (§1A: subscribe'a verilen context'e
|
|
266
|
+
# unregister inverse'i kendisi bağlar); TURN aboneliği R7 knob'unun arkasındadır.
|
|
279
267
|
def subscribe_events
|
|
280
268
|
return unless @event_bus.respond_to?(:subscribe)
|
|
281
269
|
|
|
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
270
|
@event_bus.subscribe(SUBAGENT_EVENT, context: @context) { |p| enqueue_event(:subagent, p) }
|
|
286
271
|
@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) }
|
|
272
|
+
@event_bus.subscribe(TURN_EVENT, context: @context) { |p| enqueue_event(:turn, p) } if @root_turns
|
|
292
273
|
end
|
|
293
274
|
|
|
294
275
|
# Taşma düşürmesi asla STOP işaretini yutamaz (yutsa worker hiç
|
|
@@ -335,20 +316,18 @@ module Rubik
|
|
|
335
316
|
# Önce kronik, sonra hasat; iki düzlemin hataları birbirini
|
|
336
317
|
# engellemez. Annal yazılamasa da digest tamamlanır/geri kuyruğa alınır.
|
|
337
318
|
seq = next_seq
|
|
338
|
-
line = AnnalWriter.subagent_line(seq, payload)
|
|
339
319
|
begin
|
|
340
|
-
append_session([[event_key(payload),
|
|
320
|
+
append_session([[event_key(payload), AnnalWriter.subagent_line(seq, payload)]])
|
|
341
321
|
ensure
|
|
342
322
|
@gate.completion_event(payload)
|
|
343
323
|
end
|
|
344
324
|
maintain if flush_due?
|
|
345
325
|
end
|
|
346
326
|
|
|
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).
|
|
327
|
+
# Kök-turn kroniği: ordinal session-slug süreç-başına mint edildiğinden
|
|
328
|
+
# harness-lokal sayaç yeterli (yeni süreç = yeni session annal; goal
|
|
329
|
+
# sırasındaki mağazadan-türetme burada gereksiz). Turn olayı flush
|
|
330
|
+
# sayacına sayılır ama koşulsuz maintain tetiklemez — turn sıklığı yüksek (R8).
|
|
352
331
|
def handle_turn(payload)
|
|
353
332
|
ordinal = @monitor.synchronize { @turn_ordinal += 1 }
|
|
354
333
|
seq = next_seq
|
|
@@ -360,14 +339,12 @@ module Rubik
|
|
|
360
339
|
ordinal = next_goal_ordinal
|
|
361
340
|
enriched = payload.merge(goal_ordinal: ordinal)
|
|
362
341
|
seq = next_seq
|
|
363
|
-
goal_body = [AnnalWriter.goal_header_line(enriched[:goal])] +
|
|
364
|
-
AnnalWriter.goal_lines(seq, enriched).map { |_k, l| l }
|
|
342
|
+
goal_body = [AnnalWriter.goal_header_line(enriched[:goal])] + AnnalWriter.goal_lines(seq, enriched).map { |_k, l| l }
|
|
365
343
|
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) })
|
|
344
|
+
title: AnnalWriter.goal_title(ordinal, enriched[:goal]), body: goal_body.join("\n"),
|
|
345
|
+
tags: Vocabulary::ANNAL_TAGS + [Vocabulary::GOAL_TAG], links: { "session" => Vocabulary.session_ref(@session_slug) })
|
|
369
346
|
append_session(AnnalWriter.goal_lines(seq, enriched))
|
|
370
|
-
maintain
|
|
347
|
+
maintain(goal_end: true)
|
|
371
348
|
end
|
|
372
349
|
|
|
373
350
|
# Goal sırası mağazadan türetilir (refs'teki en büyük annal.goal.<n>
|
|
@@ -384,8 +361,7 @@ module Rubik
|
|
|
384
361
|
# flush_events eşiği aşılınca true döner ve sayaç sıfırlanır.
|
|
385
362
|
def flush_due?
|
|
386
363
|
@monitor.synchronize do
|
|
387
|
-
@events_since_distill += 1
|
|
388
|
-
crossed = @events_since_distill >= @flush_events
|
|
364
|
+
crossed = (@events_since_distill += 1) >= @flush_events
|
|
389
365
|
@events_since_distill = 0 if crossed
|
|
390
366
|
crossed
|
|
391
367
|
end
|
|
@@ -393,16 +369,49 @@ module Rubik
|
|
|
393
369
|
|
|
394
370
|
# Flush bakımı (yalnızca flush noktalarında — flush_events eşiği
|
|
395
371
|
# aşımı veya goal bitişi): kaçak memory süpürme (K9) + wiki home
|
|
396
|
-
# yeniden hesap +
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
def maintain
|
|
372
|
+
# yeniden hesap + dokunulan hafıza konularının otomatik damıtımı +
|
|
373
|
+
# damıtıcı doğum kararı. v2 §4-V5: toplu doğum yalnızca kirli işaretle;
|
|
374
|
+
# hedefli damıtım TÜM konuları kuyrukladıysa toplu doğum atlanır — kayıp/red varsa batch telafi eder.
|
|
375
|
+
def maintain(goal_end: false)
|
|
400
376
|
# Sweep record() ile aynı kilide koşar (tek-yazar disiplini):
|
|
401
377
|
# get-then-put dansı çift satır açamaz.
|
|
402
378
|
swept = @monitor.synchronize { MemorySteward.sweep_rogue_memory(@store) }
|
|
403
379
|
Vakanuvis.log_via(@dmesg, :info, "steward swept #{swept} rogue memory row(s)") if swept.positive?
|
|
404
380
|
WikiSkeleton.rebuild_home(@store)
|
|
405
|
-
|
|
381
|
+
digested = auto_digest_touched!(spawn: !stopped?) if goal_end
|
|
382
|
+
@gate.flush_tick unless stopped? || digested
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Otomatik damıtım kapısı (issue #341): hedef tamamlanmasında veya
|
|
386
|
+
# kapanışta dokunulan konuları hedeflenmiş wiki derlemesi için kuyruklar;
|
|
387
|
+
# spawn=false (kapanış) zombi LLM doğumunu engeller. Konu-başı yalıtım
|
|
388
|
+
# (review #346): tek başarısız/red'li submit kalan konuları kaybettirmez —
|
|
389
|
+
# kayıplar @touched_topics'e geri alınır, bir sonraki flush noktası dener.
|
|
390
|
+
def auto_digest_touched!(spawn: true)
|
|
391
|
+
return false unless @auto_digest && @distiller_enabled
|
|
392
|
+
|
|
393
|
+
spawn_allowed = spawn && !stopped?
|
|
394
|
+
topics = @monitor.synchronize { @touched_topics.to_a.tap { @touched_topics.clear } }
|
|
395
|
+
return false if topics.empty?
|
|
396
|
+
|
|
397
|
+
stranded = topics.reject do |topic|
|
|
398
|
+
_receipt, status = @gate.submit_with_status(topic, spawn: spawn_allowed)
|
|
399
|
+
next true unless status == :full
|
|
400
|
+
|
|
401
|
+
Vakanuvis.log_via(@dmesg, :warn, "auto-digest refused for '#{topic}' (gate capacity full) — kept for the next flush point")
|
|
402
|
+
false
|
|
403
|
+
rescue StandardError => e
|
|
404
|
+
Vakanuvis.log_via(@dmesg, :warn, "auto-digest submit failed for '#{topic}' (#{e.class}: #{e.message})")
|
|
405
|
+
false
|
|
406
|
+
end
|
|
407
|
+
@monitor.synchronize { stranded.each { |t| @touched_topics << t } } unless stranded.empty?
|
|
408
|
+
unless spawn_allowed
|
|
409
|
+
queued = topics - stranded
|
|
410
|
+
note = "shutdown: #{queued.size} touched topic(s) queued but not spawned — they will not compile this session: #{queued.join(', ')}"
|
|
411
|
+
note += "; #{stranded.size} refused/failed topic(s) kept for the next flush point (warnings above)" unless stranded.empty?
|
|
412
|
+
Vakanuvis.log_via(@dmesg, :warn, note)
|
|
413
|
+
end
|
|
414
|
+
stranded.empty?
|
|
406
415
|
end
|
|
407
416
|
|
|
408
417
|
def append_session(lines)
|
|
@@ -414,18 +423,12 @@ module Rubik
|
|
|
414
423
|
@dropped = 0
|
|
415
424
|
end
|
|
416
425
|
end
|
|
417
|
-
AnnalWriter.append(@store, ref: Vocabulary.session_ref(@session_slug),
|
|
418
|
-
title: AnnalWriter.session_title(@session_slug),
|
|
419
|
-
lines: lines + overflow.to_a)
|
|
426
|
+
AnnalWriter.append(@store, ref: Vocabulary.session_ref(@session_slug), title: AnnalWriter.session_title(@session_slug), lines: lines + overflow.to_a)
|
|
420
427
|
end
|
|
421
428
|
|
|
422
429
|
def next_seq = @monitor.synchronize { @seq += 1 }
|
|
423
430
|
|
|
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
|
|
431
|
+
def event_key(payload) = "subagent.#{payload[:storage_id].to_s.empty? ? payload[:id] : payload[:storage_id]}.#{payload[:state]}"
|
|
429
432
|
|
|
430
433
|
def fetch_cfg(key, default)
|
|
431
434
|
@config.respond_to?(:fetch) ? @config.fetch(key, default) : default
|