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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8907beb9d8441f6f497af9bd1b65a998c5dfa1d2857552943809ace7270e3759
4
- data.tar.gz: a4b938aa55ff08037d63fc00d545a5039c06319c2ecee309e85d369d5d752621
3
+ metadata.gz: 9a55adca9a39a813ea7620e63018dc98ae7c902142e9a1fa1dc5afe92637ef88
4
+ data.tar.gz: 72d7ca21a839051017fd515fe63a0cc99573054ec3a23c5fd179a6e37c623460
5
5
  SHA512:
6
- metadata.gz: a034a02d012190d0f6fe02c1b7977628d60617fb0bca562c3b4799dd6512e5cb77809540d57ebe4eede099a0c8e724a679579cc75f4db85f9a8e59bd535d59ea
7
- data.tar.gz: bc8162afcace2ab99fca16b749a5291abeac662e2255f5d798277931a7295a447bdc981bb5d9bc897afa80a15db8fe836771536c3441c110208546442b280622
6
+ metadata.gz: f4b426b159392a724d6e66d96dfd09db13cee8882eec7f3dcf0afaaf3d683325d3a74731f0e34abf43e77c1f1655d850cecd2912e324e16da9ad84bd74c1eb71
7
+ data.tar.gz: 36090da4466990229e789cf5912cf880bf4193eb300f8823abc7a1cefed4bf6253aff44774ff9d18e19ebbfb3f874d7c70434d7ab72692ba474bd9e6ba337abf
data/ATTRIBUTION.md CHANGED
@@ -121,7 +121,7 @@ SOFTWARE.
121
121
  | **`strings-ansi`** | Piotr Murach | MIT | ANSI-aware string truncation and alignment for REPL formatters. |
122
122
  | **`reline`** | Hiroshi SHIBATA & Ruby Team | Ruby License / BSD-2-Clause | Line-editing, history navigation, autocompletion hooks, and headless terminal adaptation. |
123
123
  | **`prism`** | Kevin Newton & Ruby Core Team | MIT | High-performance Ruby parsing and AST extraction ([`Rubik::AST::RubyExtractor`](lib/rubik/ast/ruby_extractor.rb)). |
124
- | **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 3,900+ unit, integration, and contract tests. |
124
+ | **`minitest`** | Ryan Davis & seattlerb | MIT | Fast, modular test suite powering Rubik's 4,000+ unit, integration, and contract tests. |
125
125
  | **`rubocop`** | Bozhidar Batsov & contributors | MIT | Static code analysis, style enforcement, and architectural hygiene gates. |
126
126
  | **`benchmark`, `logger`, `pstore`, `reline`, `fiddle`** | Ruby core / rubygems teams | Ruby License / BSD-2-Clause | Ruby stdlib gems pinned explicitly in `rubik.gemspec` as they are unbundled from the default set in Ruby 3.4/3.5. |
127
127
 
data/CHANGELOG.md CHANGED
@@ -6,6 +6,52 @@ commits, 2026-08-23 → 2026-10-09, see `git log`).
6
6
 
7
7
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
8
8
 
9
+ ## [0.5.0] - 2026-10-31
10
+
11
+ A vakanüvis minor: the memory system gains a harness-owned auto-digest
12
+ loop and a deterministic topics orientation injected at session start
13
+ and after compaction, the prompt doctrine's recall/record contradiction
14
+ is closed with a single storage write path, duck-typed AST cache stores
15
+ stop breaking `save_to_cache`, and the security redactor's clean-text
16
+ path skips its intermediate allocations.
17
+
18
+ ### Added
19
+
20
+ - **Harness-owned auto-digest of touched topics (#346, #348):** topics
21
+ recorded or updated since the last digest are tracked in the Harness
22
+ and auto-digested at goal completion and queued (spawn-free) at
23
+ shutdown, gated by the `subagents.vakanuvis.auto_digest` knob
24
+ (default on); per-topic error isolation re-arms failed or refused
25
+ topics for the next flush instead of losing them, the drain window
26
+ between store write and touched-topic tracking is closed with one
27
+ monitor section, and shutdown always joins/kills the digest worker
28
+ with an honest dmesg listing parked topics.
29
+ - **Deterministic topics orientation (#343):** session start and budget
30
+ compaction inject a compact, deterministic overview of the memory
31
+ topics so the model is oriented without a recall round-trip.
32
+
33
+ ### Fixed
34
+
35
+ - **Keywords-in-query contradiction closed (#342):** the vakanüvis
36
+ teaching now separates the recall and record directions, stops
37
+ telling the model to add Keywords to recall queries, renders a single
38
+ write-path storage variant when vakanüvis is active, and folds the
39
+ doctrine down to two core duties with the duplicated
40
+ `goal_storage_rule` wording removed (SSOT).
41
+ - **Duck-typed AST cache stores (#347):** `save_to_cache` probes
42
+ `store.method(:save).parameters` once and dispatches deterministically
43
+ instead of rescuing `ArgumentError` (which swallowed genuine errors
44
+ and double-fired side-effecting stores); 1-arity stores get the
45
+ snapshot without statuses plus a warn naming the store class, while
46
+ stores declaring the canonical kwarg signature keep the full-map
47
+ delivery — including in the clean-graph case.
48
+
49
+ ### Performance
50
+
51
+ - **Redactor clean-text fast path (#337):** `Redactor.redact` no longer
52
+ allocates an intermediate scrub/dup string before testing
53
+ `QUICK_CHECK_REGEX` on valid-encoding clean input.
54
+
9
55
  ## [0.4.2] - 2026-10-29
10
56
 
11
57
  A reliability patch: plugin file loads become transactional with
@@ -961,7 +1007,8 @@ installation section.
961
1007
  - Context: serialized index mutations, snapshot-outside-lock reconcile,
962
1008
  streaming-first truncation.
963
1009
 
964
- [Unreleased]: https://github.com/ozgurulukir/rubik/compare/v0.4.2...HEAD
1010
+ [Unreleased]: https://github.com/ozgurulukir/rubik/compare/v0.5.0...HEAD
1011
+ [0.5.0]: https://github.com/ozgurulukir/rubik/compare/v0.4.2...v0.5.0
965
1012
  [0.4.2]: https://github.com/ozgurulukir/rubik/compare/v0.4.1...v0.4.2
966
1013
  [0.4.1]: https://github.com/ozgurulukir/rubik/compare/v0.4.0...v0.4.1
967
1014
  [0.1.0]: https://github.com/ozgurulukir/rubik/releases/tag/v0.1.0
@@ -110,6 +110,39 @@ module Rubik
110
110
  false
111
111
  end
112
112
 
113
+ # Dispatch a cache save across duck-typed stores. The canonical #save
114
+ # contract is (hash, statuses: {}) with a FULL statuses map (every
115
+ # snapshot file present, :ok or not), but gem consumers may hand the
116
+ # graph a narrower store. The signature is probed instead of rescuing
117
+ # ArgumentError around the call — a genuine ArgumentError raised inside
118
+ # a store's save must surface, and a failing save must never be
119
+ # silently retried single-arg (double side effects).
120
+ #
121
+ # Classification: a `statuses:` keyword (or **kwargs) means the kwarg
122
+ # call; otherwise the store is treated as 1-arity and non-ok statuses
123
+ # are dropped with a warn (such entries reload as :ok). When the
124
+ # signature cannot be read at all — save resolves through
125
+ # method_missing, or the object has no save — the canonical kwarg call
126
+ # is kept: the call itself surfaces any problem, exactly as the
127
+ # pre-probe code did. respond_to_missing?-backed proxies report
128
+ # [[:rest]] and are therefore treated as 1-arity: behavioral probing
129
+ # would risk double invocation.
130
+ def self.save_via(store, snapshot, statuses)
131
+ accepts_statuses = begin
132
+ store.method(:save).parameters.any? do |param|
133
+ kind, name = param
134
+ kind == :keyrest || ([:key, :keyreq].include?(kind) && name == :statuses)
135
+ end
136
+ rescue NameError
137
+ nil
138
+ end
139
+ return store.save(snapshot, statuses: statuses) if accepts_statuses != false
140
+
141
+ non_ok = statuses.count { |_, status| status != :ok }
142
+ Rubik::Log.warn(:ast, "#{store.class}#save cannot carry extraction statuses; #{non_ok} file(s) will reload as :ok") if non_ok.positive?
143
+ store.save(snapshot)
144
+ end
145
+
113
146
  def load
114
147
  load_with_statuses[0]
115
148
  end
@@ -139,7 +139,8 @@ module Rubik
139
139
  end
140
140
  skipped = "cache snapshot excluded #{skipped_failed} file(s) with failed extractions; they re-extract on next boot"
141
141
  Rubik::Log.warn(:ast, skipped) if skipped_failed.positive?
142
- cache_store.save(snapshot, statuses: statuses)
142
+
143
+ CacheStore.save_via(cache_store, snapshot, statuses)
143
144
  end
144
145
 
145
146
  def smart_context_for(symbol_name)
@@ -166,15 +166,21 @@ module Rubik
166
166
  # annal + wiki derleyicisi. enabled: harness kur/kurulama; distiller:
167
167
  # Düzlem 2 LLM doğumu (bütçe: flush_events x max_steps); model: nil =
168
168
  # root provider; retention: supersede|purge; root_turns: kök-turn annal'ı.
169
+ # auto_digest: goal bitişinde/kapanışta dokunulan konuların hedefli damıtımı (issue #341).
169
170
  "vakanuvis" => {
170
171
  "enabled" => true,
171
172
  "distiller" => true,
173
+ "auto_digest" => true,
172
174
  "max_steps" => 8,
173
175
  "flush_events" => 10,
174
176
  "max_digest_jobs" => 100,
175
177
  "model" => nil,
176
178
  "retention" => "supersede",
177
- "root_turns" => true
179
+ "root_turns" => true,
180
+ # Soğuk-başlangıç ve sıkıştırma sonrası yönelim enjeksiyonu (issue #340):
181
+ # sistem istemi şişirilmeden kök oturuma kısa mem.* özeti eklenir.
182
+ "orientation" => true,
183
+ "orientation_max_topics" => 10
178
184
  },
179
185
  # Pruner (repl/pruner): yarı deterministik KÖK pencere temizleyicisi,
180
186
  # DEFAULT ON (kullanıcı kararı 2026-10-10; kill-switch: enabled=false).
@@ -57,6 +57,8 @@ module Rubik
57
57
  max_output_tokens: @deps.max_output_tokens, config: context_cfg,
58
58
  absorb: GoalLedger::ObservationProjection.directive_for(deps: @deps)
59
59
  ))
60
+ manager = @deps.runtime_manager if @deps.respond_to?(:runtime_manager)
61
+ Rubik::Storage::Vakanuvis::Orientation.inject_after_compaction!(@deps.history, manager, config: @deps.config)
60
62
  end
61
63
 
62
64
  def track_token_usage!(res)
@@ -177,6 +177,7 @@ module Rubik
177
177
  end
178
178
  end
179
179
 
180
+ Rubik::Storage::Vakanuvis::Orientation.inject_session_start!(@history, @runtime_manager, config: @config)
180
181
  @history << { role: "user", content: formatted_task }
181
182
 
182
183
  execute_steps(opts: opts, plan_only: plan_only)
@@ -71,6 +71,10 @@ module Rubik
71
71
  @loop.emergency_compact_and_notify!(reason)
72
72
  end
73
73
 
74
+ def runtime_manager
75
+ @loop.runtime_manager if @loop.respond_to?(:runtime_manager)
76
+ end
77
+
74
78
  def respond_to_missing?(name, include_private = false)
75
79
  (SURFACE.include?(name) && @loop.respond_to?(name, include_private)) || super
76
80
  end
@@ -103,6 +103,8 @@ module Rubik
103
103
 
104
104
  # ── announce ──
105
105
  def announce_compaction(reason, layer)
106
+ manager = @deps.runtime_manager if @deps.respond_to?(:runtime_manager)
107
+ Rubik::Storage::Vakanuvis::Orientation.inject_after_compaction!(@deps.history, manager, config: @deps.config)
106
108
  @deps.on_step&.call(type: :emergency_compact, reason: reason, step: @deps.step_count,
107
109
  max_steps: @deps.max_steps, layer: layer)
108
110
  end
@@ -221,14 +221,15 @@ module Rubik
221
221
  def rule_nine_body(active_tools: nil, plan_mode: false)
222
222
  return "Keep the repository worktree free of agent workfiles, scratch notes, and temporary artifacts." if plan_mode
223
223
 
224
+ vakanuvis_on = tool_active?("vakanuvis", active_tools)
224
225
  storage_part = if tool_active?("storage", active_tools)
225
- Rubik::Prompt::ToolTeachings.teaching_for("storage")
226
+ Rubik::Prompt::ToolTeachings.teaching_for("storage", vakanuvis_active: vakanuvis_on)
226
227
  else
227
228
  "Keep the repository worktree free of scattered agent workfiles; read any existing " \
228
229
  "`rubik://storage/<type>/<id>` entities via `read_file` when a store is wired."
229
230
  end
230
231
  parts = [storage_part]
231
- parts << Rubik::Prompt::ToolTeachings.teaching_for("vakanuvis") if tool_active?("vakanuvis", active_tools)
232
+ parts << Rubik::Prompt::ToolTeachings.teaching_for("vakanuvis") if vakanuvis_on
232
233
  parts << Rubik::Prompt::ToolTeachings.teaching_for("show_file") if tool_active?("show_file", active_tools)
233
234
  parts << "Subagent transcripts are indexed automatically as `transcript` entities (alias `subagent-<storage-id>`) " \
234
235
  "and are readable live or persisted via the canonical `rubik://subagent/<storage-id>/transcript` reference — " \
@@ -244,14 +245,10 @@ module Rubik
244
245
  if storage_on && vakanuvis_on
245
246
  "Persist pipeline knowledge in the central storage (`storage` tool): the decomposed plan, review findings, " \
246
247
  "and the final goal record — link stages with entity ids (`links: {\"goal\": \"goal.N.<hash16>\"}`) so the " \
247
- "audit trail survives the session. Per-stage MEMORY notes go through the chronicler instead: " \
248
- "`vakanuvis(action: 'record', topic: '...', body: '...')` mints the `mem.` ref, upserts and supersedes — " \
249
- "never write memory with raw storage puts when `vakanuvis` is available " \
250
- "(fall back to `storage` `put` only when `vakanuvis` is unavailable); re-enter prior context with " \
251
- "`vakanuvis(action: 'recall', query: '...')` (also after compaction). Keep the repository worktree free of " \
252
- "agent workfiles. Transcripts of finished " \
253
- "subagents are indexed automatically (searchable via `storage` action 'search') and persist after the " \
254
- "subagent is reaped."
248
+ "audit trail survives the session. Route durable memory through the chronicler (`vakanuvis` record/recall; " \
249
+ "see rule 9), never through raw storage puts. Keep the repository worktree free of agent workfiles. " \
250
+ "Transcripts of finished subagents are indexed automatically (searchable via `storage` action 'search') " \
251
+ "and persist after the subagent is reaped."
255
252
  elsif storage_on
256
253
  "Persist pipeline knowledge in the central storage (`storage` tool): the decomposed plan, review findings, " \
257
254
  "and the final goal record — link stages with entity ids (`links: {\"goal\": \"goal.N.<hash16>\"}`) so the " \
@@ -259,11 +256,9 @@ module Rubik
259
256
  "finished subagents are indexed automatically (searchable via `storage` action 'search') and persist after " \
260
257
  "the subagent is reaped."
261
258
  elsif vakanuvis_on
262
- "Persist durable per-stage MEMORY notes through the chronicler: `vakanuvis(action: 'record', topic: '...', " \
263
- "body: '...')` mints the `mem.` ref, upserts and supersedes; re-enter prior context with " \
264
- "`vakanuvis(action: 'recall', query: '...')` (also after compaction). Keep the repository worktree free of " \
265
- "agent workfiles. Transcripts of finished subagents remain readable via `rubik://subagent/<storage-id>/transcript` " \
266
- "after the subagent is reaped."
259
+ "Route durable memory through the chronicler (`vakanuvis` record/recall; see rule 9). Keep the repository " \
260
+ "worktree free of agent workfiles. Transcripts of finished subagents remain readable via " \
261
+ "`rubik://subagent/<storage-id>/transcript` after the subagent is reaped."
267
262
  else
268
263
  "Keep the repository worktree free of agent workfiles. Transcripts of finished subagents remain readable via " \
269
264
  "`rubik://subagent/<storage-id>/transcript` after the subagent is reaped."
@@ -2,21 +2,23 @@
2
2
 
3
3
  module Rubik
4
4
  module Prompt
5
- # Tool teachings keyed by canonical tool name (architecture review
6
- # 2026-10-01, candidate 1): the sentences that teach HOW a specific tool
7
- # is used live here, one key per tool, instead of being embedded unnamed
8
- # in Doctrine.operational_rules_xml's heredoc. Identity (the tool) lives in
9
- # its Tools::Declaration; prose (the presentation) lives here — a prompt
10
- # layer concern. test_tool_teachings welds the two: every key must
11
- # resolve to a declared (Catalog) tool, so a rename that forgets the
12
- # prompt fails the suite.
13
- #
14
- # The strings are interpolated into the operational rules HEREDOC at the
15
- # same positions they originally occupied — the rendered rules are
16
- # byte-identical (pinned by the substring tests in
17
- # test_structured_context_builder, test_agent_command_contract,
18
- # test_buffer_apply_conflict, test_repl_tool_separation_and_guardrails).
5
+ # Tool teachings keyed by canonical tool name (architecture review 2026-10-01, candidate 1):
6
+ # the sentences that teach HOW a specific tool is used live here, one key per tool.
7
+ # Identity lives in its Tools::Declaration; prose lives here — a prompt layer concern.
8
+ # test_tool_teachings welds the two: every key must resolve to a declared Catalog tool.
19
9
  module ToolTeachings
10
+ STORAGE_BASE =
11
+ "Persist agent workfiles — goals, plans, reviews, and work notes — in the central " \
12
+ "`storage` database (`storage` tool: put/get/list/search/set_ref/delete), NOT as scattered files in " \
13
+ "the repository worktree. Read, list, and search all entity types (`goal`, `plan`, `memory`, `wiki`, " \
14
+ "`transcript`, `review`, `note`) via `storage` or `read_file(path: 'rubik://storage/<type>/<id>')` " \
15
+ "(browse with `rubik://storage/index` and `rubik://storage/<type>` listings)."
16
+
17
+ STORAGE_FALLBACK =
18
+ "#{STORAGE_BASE[0...-1]}; write durable `memory` cards and compiled `wiki` pages through direct " \
19
+ "`storage` `put` on `memory`/`wiki` (reserving direct `storage` `put` on `memory`/`wiki` for sessions " \
20
+ "where `vakanuvis` is unavailable).".freeze
21
+
20
22
  TEACHINGS = {
21
23
  "replace_file_content" =>
22
24
  "For modifying existing files, prefer `replace_file_content` with `path`, exact verbatim `target_content`, " \
@@ -35,15 +37,7 @@ module Rubik
35
37
  "params_schema: { type: 'object', properties: { ... }, required: [...] }, code: '->(args) { ... }')` " \
36
38
  "(always provide `params_schema` when the handler reads arguments from `args`; omit it or pass an empty " \
37
39
  "`{ type: 'object', properties: {} }` schema only for tools that take no arguments).",
38
- "storage" =>
39
- "Persist agent workfiles — goals, plans, reviews, and work notes — in the central " \
40
- "`storage` database (`storage` tool: put/get/list/search/set_ref/delete), NOT as scattered files in " \
41
- "the repository worktree. Read, list, and search all entity types (`goal`, `plan`, `memory`, `wiki`, " \
42
- "`transcript`, `review`, `note`) via `storage` or `read_file(path: 'rubik://storage/<type>/<id>')` " \
43
- "(browse with `rubik://storage/index` and `rubik://storage/<type>` listings); write durable `memory` " \
44
- "cards and compiled `wiki` pages through `vakanuvis` (`record` / `digest`) whenever `vakanuvis` is in " \
45
- "your toolbelt, reserving direct `storage` `put` on `memory`/`wiki` for sessions where `vakanuvis` is " \
46
- "unavailable.",
40
+ "storage" => STORAGE_BASE,
47
41
  "run_command" =>
48
42
  "GitHub facts must be verified, not assumed: after `gh pr merge` (which can succeed with EMPTY output) " \
49
43
  "confirm the merge with `gh pr view <number> --json state`; `mergeable: UNKNOWN` right after a push means " \
@@ -57,17 +51,13 @@ module Rubik
57
51
  "while your context stays lean; only fall back to read_file when YOU must reason about the content.",
58
52
  "vakanuvis" =>
59
53
  "The background chronicler keeps compiled knowledge (memory cards, wiki pages) in rubik://storage. " \
60
- "At task start — and again after any compaction — ask it with `vakanuvis(action: 'recall', query: '...')` " \
61
- "instead of pre-loading pages: it returns a deterministic digest plus rubik:// source links. When " \
62
- "dispatching a subagent, paste the relevant recall digest (or its rubik:// link) into the task brief. " \
63
- "At the end of a task, persist only the durable knowledge with `vakanuvis(action: 'record', topic: '...', " \
64
- "body: '...')` — memory writes go through record (ref minting, upsert, supersede come free), never " \
65
- "through raw storage puts when `vakanuvis` is available. Recall is keyword-AND over card/page text, so END " \
66
- "every recorded body and digest query with its own vocabulary: include a `Keywords:` line listing 5-10 " \
67
- "synonyms in BOTH working languages (e.g. TR + EN) — a Turkish query cannot otherwise find an English page. " \
68
- "If recall is too shallow, queue `vakanuvis(action: 'digest', query: '...')`: it returns a deterministic " \
69
- "receipt and compiles the wiki page asynchronously. Make the session close a standard digest point: review " \
70
- "the session's new cards, queue one digest per open topic, and let the home index refresh.",
54
+ "At task start, ask it with `vakanuvis(action: 'recall', query: '...')`: it returns a deterministic " \
55
+ "digest plus rubik:// source links. Recall is keyword-AND over card/page text: use 2-3 core terms. " \
56
+ "If the result is empty, retry once with other-language synonyms. When dispatching a subagent, paste " \
57
+ "the relevant recall digest (or its rubik:// link) into the task brief. At the end of a task, persist " \
58
+ "durable conclusions with `vakanuvis(action: 'record', topic: '...', body: '...')` — memory writes go " \
59
+ "through record (ref minting, upsert, supersede come free), never through raw storage puts. End the body " \
60
+ "with a `Keywords:` line (TR+EN) if you can; it is auto-extracted otherwise.",
71
61
  "prune_context" =>
72
62
  "When long sessions leave stale exploration and superseded tool output weighing on the ROOT window, " \
73
63
  "check `prune_context(action: 'status')` (window estimate vs ceiling, staged plan state) and force a " \
@@ -79,7 +69,11 @@ module Rubik
79
69
 
80
70
  module_function
81
71
 
82
- def teaching_for(tool_name) = TEACHINGS[tool_name.to_s]
72
+ def teaching_for(tool_name, vakanuvis_active: true)
73
+ return vakanuvis_active ? STORAGE_BASE : STORAGE_FALLBACK if tool_name.to_s == "storage"
74
+
75
+ TEACHINGS[tool_name.to_s]
76
+ end
83
77
  end
84
78
  end
85
79
  end
@@ -97,6 +97,7 @@ module Rubik
97
97
  config: context_cfg,
98
98
  force: true
99
99
  )
100
+ Rubik::Storage::Vakanuvis::Orientation.inject_after_compaction!(ctx.conversation_history, ctx.runtime_manager, config: ctx.config)
100
101
  after_tokens = Rubik::Prompt::TokenEstimator.estimate_history(ctx.conversation_history)
101
102
  delta = before_tokens - after_tokens
102
103
  ctx.out.puts "Context compaction completed: #{before_tokens} -> #{after_tokens} tokens (reduced #{delta} tokens)."
@@ -57,11 +57,13 @@ module Rubik
57
57
 
58
58
  shell.register_command("clear", description: "Clear conversation history / dialogue turns") do |_args, ctx|
59
59
  ctx.conversation_history.clear
60
+ Rubik::Storage::Vakanuvis::Orientation.reset_session!(ctx.runtime_manager)
60
61
  ctx.out.puts "Conversation context cleared. Starting fresh topic."
61
62
  end
62
63
 
63
64
  shell.register_command("reset", description: "Reset conversation history and in-memory dialogue turns") do |_args, ctx|
64
65
  ctx.conversation_history.clear
66
+ Rubik::Storage::Vakanuvis::Orientation.reset_session!(ctx.runtime_manager)
65
67
  ctx.out.puts "Conversation context reset. Starting fresh topic."
66
68
  end
67
69
 
@@ -191,6 +191,8 @@ module Rubik
191
191
  def apply_staged_prune_plan!
192
192
  return if @replaying
193
193
 
194
+ Rubik::Storage::Vakanuvis::Orientation.inject_session_start!(@conversation_history, @runtime_manager, config: @config) if @conversation_history.empty?
195
+
194
196
  harness = Rubik::Coeffects.get(@runtime_manager&.context, Rubik::Coeffects::PRUNER)
195
197
  return unless harness
196
198
 
@@ -198,6 +200,7 @@ module Rubik
198
200
  return unless new_window
199
201
 
200
202
  @conversation_history = new_window
203
+ Rubik::Storage::Vakanuvis::Orientation.inject_after_compaction!(@conversation_history, @runtime_manager, config: @config)
201
204
  line = "[Pruner] Applied staged plan: dropped #{receipt[:dropped]} unit(s), " \
202
205
  "summarized #{receipt[:summarized]} (#{receipt[:skipped_stale]} stale op(s) skipped) — " \
203
206
  "window ~#{receipt[:before_tokens]} -> ~#{receipt[:after_tokens]} tokens."
@@ -38,16 +38,20 @@ module Rubik
38
38
  return text unless text.is_a?(String)
39
39
  return text if text.empty?
40
40
 
41
- # scrub: redact ham geçmişe (tool çıktısı, prompt) dokunur — geçersiz
42
- # UTF-8 gsub!'ı ArgumentError ile düşürür; total olmak maskenin
43
- # kendisi bir arıza kaynağı olmasın demektir.
44
- result = text.valid_encoding? ? text.dup : text.scrub
41
+ # PERF: Fast-path for valid encoding strings: test QUICK_CHECK_REGEX
42
+ # directly on input text before calling dup or scrub. This short-circuits
43
+ # clean strings without allocating extra intermediate string copies or
44
+ # running scrub/gsub passes (~2.7x speedup, ~50% fewer allocations).
45
+ if text.valid_encoding?
46
+ return text.dup unless text.match?(QUICK_CHECK_REGEX)
45
47
 
46
- # PERF: kısa devre — sır tetikleyicisi yoksa 11 gsub turu atlanır.
47
- # Tetik testi scrub'lanmış sonuç üstünde yapılır: match? ham geçersiz
48
- # UTF-8'de ArgumentError düşürür; scrub sonrası metin hep geçerlidir
49
- # ve desenler zaten bu metne uygulanıyordu, dolayısıyla eşdeğerdir.
50
- return result unless result.match?(QUICK_CHECK_REGEX)
48
+ result = text.dup
49
+ else
50
+ # Fall back safely for invalid UTF-8 / binary strings: scrub first so
51
+ # match? and gsub! do not raise ArgumentError.
52
+ result = text.scrub
53
+ return result unless result.match?(QUICK_CHECK_REGEX)
54
+ end
51
55
 
52
56
  # Keep replacement order: masking one credential can expose a word
53
57
  # boundary needed to recognize the next (for example, a bearer token).
@@ -54,8 +54,11 @@ module Rubik
54
54
  # hemen ardından doğum denemesi AYRI bir @lock penceresinde
55
55
  # (iki fazlı — tek büyük kritik bölüm İÇİNE ALMA). Doğum
56
56
  # başarısızsa iş kuyrukta kalır: yarım durum birikmez.
57
- # Dönen makbuz dizgesi bayt-bayt eski Harness#digest çıktısıdır.
58
- def submit(query)
57
+ # Dönen makbuz dizgesi bayt-bayt eski Harness#digest çıktısıdır;
58
+ # submit_with_status aynı kabulü [makbuz, durum] ikilisi olarak
59
+ # verir (:accepted|:duplicate|:full) — otomatik damıtım red'i
60
+ # makbuz dizgesinden ayırt edebilsin diye (review #346).
61
+ def submit_with_status(query, spawn: true)
59
62
  target = Vocabulary.wiki_ref(query)
60
63
  # v2 §4-V5: aynı hedefli iş zaten kuyrukta ya da uçuştaysa makbuz
61
64
  # 'already queued' varyantına düşer — çift doğum yok.
@@ -68,18 +71,20 @@ module Rubik
68
71
  :accepted
69
72
  end
70
73
  if admission == :duplicate
71
- return "Digest already queued for '#{target}' (#{AnnalWriter.one_line(query, 60)}): the chronicler is " \
72
- "compiling it — recall it later with vakanuvis(action: 'recall', query: '...')."
74
+ return ["Digest already queued for '#{target}' (#{AnnalWriter.one_line(query, 60)}): the chronicler is " \
75
+ "compiling it — recall it later with vakanuvis(action: 'recall', query: '...').", admission]
73
76
  end
74
77
  if admission == :full
75
- return "Digest not accepted for '#{target}': capacity #{@max_digest_jobs} is full. " \
76
- "Existing jobs are preserved; retry after a digest finishes."
78
+ return ["Digest not accepted for '#{target}': capacity #{@max_digest_jobs} is full. " \
79
+ "Existing jobs are preserved; retry after a digest finishes.", admission]
77
80
  end
78
81
 
79
- spawned = spawn_job!(batch: false)
80
- DistillGate.receipt(query: query, target: target, spawned: !spawned.nil?)
82
+ spawned = spawn ? spawn_job!(batch: false) : nil
83
+ [DistillGate.receipt(query: query, target: target, spawned: !spawned.nil?), admission]
81
84
  end
82
85
 
86
+ def submit(query, spawn: true) = submit_with_status(query, spawn: spawn)[0]
87
+
83
88
  # ---- Seam 2: worker thread'i (Harness#handle_subagent çağırır) ---
84
89
 
85
90
  # Ham subagent tamamlanma olayı. İş sicilden @lock altında