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
|
@@ -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
|
|
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.
|
|
248
|
-
"
|
|
249
|
-
"
|
|
250
|
-
"
|
|
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
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"`
|
|
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."
|
|
@@ -52,9 +52,13 @@ module Rubik
|
|
|
52
52
|
# "head_tail" (default, original behavior) keeps head + tail around the
|
|
53
53
|
# omission notice; "head_only" keeps the message head — useful when the
|
|
54
54
|
# actionable guidance at the tail must not crowd out the error header.
|
|
55
|
-
# Truncator normalizes strings/symbols
|
|
56
|
-
# unknown values
|
|
57
|
-
|
|
55
|
+
# Truncator.resolve_strategy normalizes strings/symbols, falls back to
|
|
56
|
+
# head_tail on unknown values AND warns once — the same typo-honesty
|
|
57
|
+
# contract as the tools.truncation_strategy knob (issue #349).
|
|
58
|
+
strategy = Rubik::Tools::Truncator.resolve_strategy(
|
|
59
|
+
config["emergency_truncate_strategy"] || config[:emergency_truncate_strategy],
|
|
60
|
+
key: "context.emergency_truncate_strategy"
|
|
61
|
+
)
|
|
58
62
|
|
|
59
63
|
history.each do |msg|
|
|
60
64
|
next unless ConversationRows.row?(msg)
|
|
@@ -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
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"body: '...')` — memory writes go
|
|
65
|
-
"through
|
|
66
|
-
"
|
|
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
|
|
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)."
|
|
@@ -270,6 +270,13 @@ module Rubik
|
|
|
270
270
|
private_class_method def self.suspend_goal_turn(ctx, orchestrator:, goal_state:, goal_ledger:,
|
|
271
271
|
goal_text:, goal_id:, record_pair:, reason:, tokens: nil)
|
|
272
272
|
attempt = suspended_attempt(ctx, goal_id) + 1
|
|
273
|
+
# Ledger kimliği askıdan ÖNCE okunur: suspend! → ledger.suspend!
|
|
274
|
+
# sırası metanın bu turun token'larını içermesi için zorunlu,
|
|
275
|
+
# ama o damga storage_id'yi değiştirdiği için sonradan atanan id
|
|
276
|
+
# SUSPEND_EVENT'e geç kalırdı — ilk askıda kronik satırı slug
|
|
277
|
+
# kimliğine düşer, ledger= bağlantısı kaybolurdu.
|
|
278
|
+
# resume_cross_process ile aynı uçuş-içi atama önceliği.
|
|
279
|
+
goal_state.ledger_id = goal_ledger.snapshot[:storage_id] if goal_ledger
|
|
273
280
|
goal_state.suspend!(reason, tokens: tokens)
|
|
274
281
|
goal_ledger&.suspend!(suspend_stamp_meta(goal_state, goal_id, reason, attempt))
|
|
275
282
|
registry = goal_registry_for(ctx)
|
|
@@ -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
|
|
|
@@ -82,7 +82,11 @@ module Rubik
|
|
|
82
82
|
clean_raw = Rubik::Security::Redactor.redact(parsed.raw)
|
|
83
83
|
@recorder&.record_repl_command(clean_raw, output_text)
|
|
84
84
|
# Plan 61 D-8: bound what a bang command leaves in LLM memory; the recorder keeps the full text.
|
|
85
|
-
|
|
85
|
+
max_chars = @config.fetch("tools.max_output_chars", Rubik::Tools::Truncator::DEFAULT_MAX_CHARS)
|
|
86
|
+
# Deliberately context-scoped (issue #349 F8): truncation_strategy is a
|
|
87
|
+
# tool-output contract (the two orchestrator planes); bang-command echo
|
|
88
|
+
# is REPL history capture and stays head_tail, outside the knob's promise.
|
|
89
|
+
trunc = Rubik::Tools::Truncator.truncate(output_text, max_chars: max_chars, strategy: :head_tail)
|
|
86
90
|
@conversation_history << { role: "user", content: clean_raw.delete_prefix("!") } # bangs stay out of LLM history; recorder keeps the literal
|
|
87
91
|
@conversation_history << { role: "assistant", content: trunc[:truncated] ? trunc[:text] : output_text }
|
|
88
92
|
trim_conversation_window!
|
|
@@ -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
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module Rubik
|
|
4
6
|
module Storage
|
|
5
7
|
module Vakanuvis
|
|
@@ -35,6 +37,44 @@ module Rubik
|
|
|
35
37
|
[["goal.#{payload[:goal_ordinal]}.#{state}", "#{head} (event=goal.#{payload[:goal_ordinal]}.#{state})"]]
|
|
36
38
|
end
|
|
37
39
|
|
|
40
|
+
# Askı satırı (issue #350): anahtar PEEK sırası DEĞİL hedef
|
|
41
|
+
# kimliğidir. Askı peek'lediği sıraya ait görünür; araya başka bir
|
|
42
|
+
# goal bitişi girerse peek yanlış kayda bağlanır, iki askı aynı
|
|
43
|
+
# peek'i paylaşınca dedup ikinci satırı yutar. Kimlik = ledger_id
|
|
44
|
+
# (goal.ledger açıkken post_lifecycle'nin damgası), yoksa goal
|
|
45
|
+
# metninin slug'ı; steps + hata özeti anahtara işlenir — gerçek
|
|
46
|
+
# yeniden teslim düşer, içerik değişen askı kalır. Satır goal=
|
|
47
|
+
# tek-satırını da taşır: sıra bağlantısı kalkınca metnin kendisi
|
|
48
|
+
# dürüst bağdır (goal kaydı askıda henüz mint edilmez).
|
|
49
|
+
def suspend_line(seq, payload)
|
|
50
|
+
ledger = payload[:ledger_id].to_s
|
|
51
|
+
identity = ledger.empty? ? Vocabulary.slug(payload[:goal]) : ledger
|
|
52
|
+
head = "- [#{seq}] goal status=suspended steps=#{payload[:steps]}/#{payload[:max_steps]}"
|
|
53
|
+
head += " error=#{one_line(payload[:last_error], 80)}" unless payload[:last_error].to_s.empty?
|
|
54
|
+
head += " ledger=#{one_line(ledger, 40)}" unless ledger.empty?
|
|
55
|
+
head += " goal=#{one_line(payload[:goal], 60)}"
|
|
56
|
+
error = payload[:last_error].to_s
|
|
57
|
+
err_part = error.empty? ? "ok" : Digest::SHA256.hexdigest(error)[0, 8]
|
|
58
|
+
key = "goal.suspend.#{identity}.#{payload[:steps]}.#{err_part}"
|
|
59
|
+
[[key, "#{head} (event=#{key})"]]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Olay-kimliği eki (issue #351 F7): handle_event'in kurtarma
|
|
63
|
+
# günlüğü tek başına kind ile askı satırını bitiş satırından
|
|
64
|
+
# ayırt edemez — düşen kronik satırının kimliği status + goal
|
|
65
|
+
# tek-satırıdır (bitiş ayrıca maintain(goal_end: true)'i atlar).
|
|
66
|
+
# Üretilemezse boş dizgi; günlük satırı asla patlamaz.
|
|
67
|
+
def event_context(kind, payload)
|
|
68
|
+
return "" unless payload.is_a?(Hash)
|
|
69
|
+
|
|
70
|
+
case kind
|
|
71
|
+
when :goal then " status=#{payload[:status]} goal=#{one_line(payload[:goal], 40)}"
|
|
72
|
+
when :subagent then " id=#{payload[:storage_id] || payload[:id]} state=#{payload[:state]}"
|
|
73
|
+
when :turn then " kind=#{payload[:kind]} prompt=#{one_line(payload[:prompt], 40)}"
|
|
74
|
+
else ""
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
38
78
|
# Kök-turn satırı (root-turn planı 2026-10-04): REPL turn'ünün
|
|
39
79
|
# deterministik özeti. Boş alan (tools/tokens) satırda görünmez;
|
|
40
80
|
# ordinal harness-lokal sayaçtan gelir, anahtar turn.<n>.<kind>.
|
|
@@ -64,13 +104,20 @@ module Rubik
|
|
|
64
104
|
"goal: #{text}"
|
|
65
105
|
end
|
|
66
106
|
|
|
67
|
-
# Satır ekleme + tekilleştirme: body'de
|
|
68
|
-
# daha yazılmaz (olay yeniden teslimi — resume
|
|
69
|
-
# tamamlanma gibi — yinelenen satır açmaz).
|
|
107
|
+
# Satır ekleme + tekilleştirme: body'de SATIR SONU (event=anahtar)
|
|
108
|
+
# taşıyan satır bir daha yazılmaz (olay yeniden teslimi — resume
|
|
109
|
+
# sonrası ikinci tamamlanma gibi — yinelenen satır açmaz). Bağ
|
|
110
|
+
# (end_with?) zorunludur: serbest metin alanları (goal=, error=,
|
|
111
|
+
# prompt= …) bir "(event=X)" dizgisi TAŞIYABİLİR; gövde-geniş
|
|
112
|
+
# include taraması o dizgiyi sahte anahtar sayıp gelecekteki
|
|
113
|
+
# gerçek X satırını sessizce yutardı (review #353: zehirlenmiş
|
|
114
|
+
# dedup). Anahtar daima satırı bitirdiğinden bağ tarama hem dürüst
|
|
115
|
+
# hem ucuzdur.
|
|
70
116
|
def append(store, ref:, title:, tags: Vocabulary::ANNAL_TAGS, links: {}, lines: [])
|
|
71
117
|
existing = store.get(type: "note", id: ref)
|
|
72
118
|
body = existing ? existing["body"].to_s : ""
|
|
73
|
-
|
|
119
|
+
settled = body.lines.map(&:rstrip)
|
|
120
|
+
fresh = lines.reject { |key, _line| settled.any? { |l| l.end_with?("(event=#{key})") } }
|
|
74
121
|
return existing if existing && fresh.empty?
|
|
75
122
|
|
|
76
123
|
merged = ([body] + fresh.map { |_key, line| line }).reject { |l| l.to_s.strip.empty? }.join("\n")
|
|
@@ -25,11 +25,13 @@ module Rubik
|
|
|
25
25
|
# v2 §4-V6: öneri sonundaki smoke-test sorgularının sayı sınırı.
|
|
26
26
|
CHECK_LIMIT = 5
|
|
27
27
|
|
|
28
|
-
def receipt(query:, target:, spawned:)
|
|
28
|
+
def receipt(query:, target:, spawned:, spawn_failed: false)
|
|
29
29
|
head = "Digest queued (async): the chronicler will compile '#{AnnalWriter.one_line(query, 80)}' into '#{target}'."
|
|
30
30
|
tail = if spawned
|
|
31
31
|
"This call did NOT wait for the LLM — conclude your turn; recall it later with " \
|
|
32
32
|
"vakanuvis(action: 'recall', query: '...') once compiled."
|
|
33
|
+
elsif spawn_failed
|
|
34
|
+
"The distiller spawn attempt failed (see dmesg) — the job stays queued and the spawn retries with the next digest or flush."
|
|
33
35
|
else
|
|
34
36
|
"No distiller run started yet (subagents.vakanuvis.distiller or the chronicler type may be " \
|
|
35
37
|
"disabled) — the job stays queued and compiles when the distiller becomes available."
|
|
@@ -37,6 +39,16 @@ module Rubik
|
|
|
37
39
|
"#{head} #{tail}"
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
def duplicate_receipt(query, target)
|
|
43
|
+
"Digest already queued for '#{target}' (#{AnnalWriter.one_line(query, 60)}): the chronicler is " \
|
|
44
|
+
"compiling it — recall it later with vakanuvis(action: 'recall', query: '...')."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def full_receipt(target, capacity)
|
|
48
|
+
"Digest not accepted for '#{target}': capacity #{capacity} is full. " \
|
|
49
|
+
"Existing jobs are preserved; retry after a digest finishes."
|
|
50
|
+
end
|
|
51
|
+
|
|
40
52
|
# Öneri şeması: ilk satır hedef ref (wiki.<slug>), gerisi gövde;
|
|
41
53
|
# gövde sonunda isteğe bağlı `CHECK: <sorgu>` satırları (v2 §4-V6
|
|
42
54
|
# smoke testi — en fazla CHECK_LIMIT adet, gövdeden AYIKLANIR,
|
|
@@ -128,7 +140,7 @@ module Rubik
|
|
|
128
140
|
"read_file(path: '#{annal_uri}')"
|
|
129
141
|
end
|
|
130
142
|
Array(job[:transcript_uris]).each { |u| sources << "- Transcript: read_file(path: '#{u}')" }
|
|
131
|
-
sources << "- Goal record: read_file(path: '#{store.uri_for('
|
|
143
|
+
sources << "- Goal record: read_file(path: '#{store.uri_for('note', Vocabulary.goal_ref(job[:goal_ordinal]))}')" if job[:goal_ordinal]
|
|
132
144
|
if (gaps = open_recall_gaps(store))
|
|
133
145
|
sources << gaps
|
|
134
146
|
end
|
|
@@ -54,8 +54,13 @@ 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
|
-
|
|
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
|
+
# goal_ordinal: goal-end auto-digest'in kanıt hedefi (tarama kartı 1,
|
|
62
|
+
# 2026-11-01) — işe işlenir, brief goal kaydını kaynak gösterir.
|
|
63
|
+
def submit_with_status(query, spawn: true, goal_ordinal: nil)
|
|
59
64
|
target = Vocabulary.wiki_ref(query)
|
|
60
65
|
# v2 §4-V5: aynı hedefli iş zaten kuyrukta ya da uçuştaysa makbuz
|
|
61
66
|
# 'already queued' varyantına düşer — çift doğum yok.
|
|
@@ -63,23 +68,22 @@ module Rubik
|
|
|
63
68
|
next :duplicate if @digest_targets.key?(target)
|
|
64
69
|
next :full if @digest_targets.size >= @max_digest_jobs
|
|
65
70
|
|
|
66
|
-
@pending_digests <<
|
|
71
|
+
@pending_digests << Job.new(query: query, target: target, transcript_uris: source_transcript_uris, goal_ordinal: goal_ordinal)
|
|
67
72
|
@digest_targets[target] = true
|
|
68
73
|
:accepted
|
|
69
74
|
end
|
|
70
|
-
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: '...')."
|
|
73
|
-
end
|
|
74
|
-
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."
|
|
77
|
-
end
|
|
75
|
+
return [DistillGate.duplicate_receipt(query, target), admission] if admission == :duplicate
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
return [DistillGate.full_receipt(target, @max_digest_jobs), admission] if admission == :full
|
|
78
|
+
|
|
79
|
+
spawned = spawn ? spawn_job!(batch: false) : nil
|
|
80
|
+
# REVIEW: patlak doğum "disabled" makbuzuyla yalanlanmasın (F2).
|
|
81
|
+
failed_spawn = spawn && spawned.nil? && spawn_allowed?
|
|
82
|
+
[DistillGate.receipt(query: query, target: target, spawned: !spawned.nil?, spawn_failed: failed_spawn), admission]
|
|
81
83
|
end
|
|
82
84
|
|
|
85
|
+
def submit(query, spawn: true) = submit_with_status(query, spawn: spawn)[0]
|
|
86
|
+
|
|
83
87
|
# ---- Seam 2: worker thread'i (Harness#handle_subagent çağırır) ---
|
|
84
88
|
|
|
85
89
|
# Ham subagent tamamlanma olayı. İş sicilden @lock altında
|
|
@@ -145,7 +149,7 @@ module Rubik
|
|
|
145
149
|
# Salt-okunuma peeki (test/kurcalama yüzeyi): kuyruğun başını
|
|
146
150
|
# gösterir — tüketmez. Üretim görünürlüğü aşağıdaki job_targets'tır.
|
|
147
151
|
def peek_job(batch: false)
|
|
148
|
-
@lock.synchronize { @pending_digests.first || (batch ?
|
|
152
|
+
@lock.synchronize { @pending_digests.first || (batch ? Job.new(batch: true) : nil) }
|
|
149
153
|
end
|
|
150
154
|
|
|
151
155
|
# İş defteri görünümü (issue #100 P2b): kuyruktaki + uçuştaki
|
|
@@ -214,11 +218,15 @@ module Rubik
|
|
|
214
218
|
@supervisor.respond_to?(:spawn) &&
|
|
215
219
|
@supervisor.respond_to?(:type_registry) && @supervisor.type_registry.get(TYPE_NAME)
|
|
216
220
|
end
|
|
221
|
+
# Genel durum sorgusu (inceleme turu 2): Harness, kabul edilen
|
|
222
|
+
# digest topic'lerinin hiç derlenemeyeceğini dürüstçe
|
|
223
|
+
# bildirmek için doğum kapısını sorabilir olmalı.
|
|
224
|
+
public :spawn_allowed?
|
|
217
225
|
|
|
218
226
|
# Aşağısı YALNIZ @lock altından çağrılır (çağıran: submit /
|
|
219
227
|
# spawn_job! / completion_event) — kendi kilidi almaz.
|
|
220
228
|
def claim_job(batch: false)
|
|
221
|
-
@pending_digests.shift || (batch ?
|
|
229
|
+
@pending_digests.shift || (batch ? Job.new(batch: true, transcript_uris: source_transcript_uris) : nil)
|
|
222
230
|
end
|
|
223
231
|
|
|
224
232
|
def restore_job(job)
|