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.
@@ -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 { harness.shutdown }
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
- Rubik::Log.warn(SUBSYSTEM, message)
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)
@@ -74,4 +80,5 @@ require_relative "vakanuvis/wiki_skeleton"
74
80
  require_relative "vakanuvis/distill_gate"
75
81
  require_relative "vakanuvis/gate"
76
82
  require_relative "vakanuvis/harness"
83
+ require_relative "vakanuvis/orientation"
77
84
  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
- "and after compaction), 'record' (persist durable knowledge as a memory card; the ONE " \
29
- "path for memory writes), 'forget' (supersede/purge a mem.* card or a note), 'digest' (queue an " \
30
- "async wiki compilation; returns a deterministic receipt, never blocks on the LLM), " \
31
- "'topics' (list recorded memory topics — orient before the first query), 'digest_status' " \
32
- "(is my queued digest pending, in flight, or done?). Notes (annal.*) are the chronicler's " \
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: { type: "string",
46
- description: "recall/digest: knowledge query. Recall is keyword-AND — every space-separated " \
47
- "term must literally appear; retry with other-language synonyms when empty." },
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 so " \
56
- "keyword-AND recall can find the card — a missing Keywords: line is auto-extracted " \
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" },
data/lib/rubik/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubik
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.0"
5
5
  end
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.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Özgür Ulukır
@@ -721,6 +721,7 @@ files:
721
721
  - lib/rubik/storage/vakanuvis/gate.rb
722
722
  - lib/rubik/storage/vakanuvis/harness.rb
723
723
  - lib/rubik/storage/vakanuvis/memory_steward.rb
724
+ - lib/rubik/storage/vakanuvis/orientation.rb
724
725
  - lib/rubik/storage/vakanuvis/service.rb
725
726
  - lib/rubik/storage/vakanuvis/vocabulary.rb
726
727
  - lib/rubik/storage/vakanuvis/wiki_skeleton.rb