Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0d0923488 | ||
|
|
47c2e56393 | ||
|
|
004d168157 | ||
|
|
87e1320175 | ||
|
|
7a27ed557c | ||
|
|
ec20649e35 | ||
|
|
303848a0fb |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.110.0**
|
||||
**Current release: v0.111.0**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.110.0"
|
||||
version = "0.111.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// Replaces src/rune/subsystem_health.py.
|
||||
//
|
||||
// Usage:
|
||||
// subsystem-health record <subsystem> <error> <tier>
|
||||
// subsystem-health snapshot
|
||||
// subsystem-health clear
|
||||
//
|
||||
// subsystem-health record <subsystem> <error> <tier>
|
||||
// subsystem-health snapshot
|
||||
// subsystem-health clear
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -24,15 +25,15 @@ type Failure struct {
|
||||
}
|
||||
|
||||
type HealthRegistry struct {
|
||||
mu sync.Mutex
|
||||
Failures map[string][]Failure `json:"failures"`
|
||||
mu sync.Mutex
|
||||
Failures map[string][]Failure `json:"failures"`
|
||||
}
|
||||
|
||||
type HealthStatus struct {
|
||||
Subsystem string `json:"subsystem"`
|
||||
TotalCount int `json:"total_count"`
|
||||
LastTier int `json:"last_tier"`
|
||||
LastError string `json:"last_error"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
TotalCount int `json:"total_count"`
|
||||
LastTier int `json:"last_tier"`
|
||||
LastError string `json:"last_error"`
|
||||
LastTimestamp string `json:"last_timestamp"`
|
||||
}
|
||||
|
||||
@@ -44,22 +45,23 @@ func healthPath() string {
|
||||
return filepath.Join(home, "subsystem_health.json")
|
||||
}
|
||||
|
||||
func loadRegistry() HealthRegistry {
|
||||
var hr HealthRegistry
|
||||
hr.Failures = make(map[string][]Failure)
|
||||
func loadRegistry() *HealthRegistry {
|
||||
hr := &HealthRegistry{Failures: make(map[string][]Failure)}
|
||||
data, err := os.ReadFile(healthPath())
|
||||
if err != nil {
|
||||
return hr
|
||||
}
|
||||
json.Unmarshal(data, &hr)
|
||||
json.Unmarshal(data, hr)
|
||||
return hr
|
||||
}
|
||||
|
||||
func saveRegistry(hr HealthRegistry) {
|
||||
hr.mu.Lock()
|
||||
defer hr.mu.Unlock()
|
||||
func saveRegistry(hr *HealthRegistry) {
|
||||
// Marshal ONLY durable state — the embedded sync.Mutex must never be
|
||||
// copied (go vet copies-lock) and is not persisted state.
|
||||
data, _ := json.MarshalIndent(struct {
|
||||
Failures map[string][]Failure `json:"failures"`
|
||||
}{Failures: hr.Failures}, "", " ")
|
||||
os.MkdirAll(filepath.Dir(healthPath()), 0755)
|
||||
data, _ := json.MarshalIndent(hr, "", " ")
|
||||
os.WriteFile(healthPath(), data, 0644)
|
||||
}
|
||||
|
||||
@@ -95,13 +97,15 @@ func main() {
|
||||
case "snapshot":
|
||||
statuses := []HealthStatus{}
|
||||
for subsystem, failures := range hr.Failures {
|
||||
if len(failures) == 0 { continue }
|
||||
if len(failures) == 0 {
|
||||
continue
|
||||
}
|
||||
last := failures[len(failures)-1]
|
||||
statuses = append(statuses, HealthStatus{
|
||||
Subsystem: subsystem,
|
||||
TotalCount: len(failures),
|
||||
LastTier: last.Tier,
|
||||
LastError: last.Error,
|
||||
Subsystem: subsystem,
|
||||
TotalCount: len(failures),
|
||||
LastTier: last.Tier,
|
||||
LastError: last.Error,
|
||||
LastTimestamp: last.Timestamp.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
// score_neuron, route, and the warm neuron listing.
|
||||
//
|
||||
// Usage:
|
||||
// warm-router base-level <timestamps-csv> <now-iso>
|
||||
// warm-router score <neuron-json> <text> [--session-context <ctx>]
|
||||
// warm-router route <text> [--max-tokens N]
|
||||
//
|
||||
// warm-router base-level <timestamps-csv> <now-iso>
|
||||
// warm-router score <neuron-json> <text> [--session-context <ctx>]
|
||||
// warm-router route <text> [--max-tokens N]
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -58,11 +59,13 @@ func baseLevelFromUses(usedAtList []string, nowISO string) float64 {
|
||||
// ── Neuron scoring (parity: warm_router.py:487) ──
|
||||
|
||||
type WarmNeuron struct {
|
||||
Slug string `json:"slug"`
|
||||
Triggers []string `json:"triggers"`
|
||||
Text string `json:"text"`
|
||||
Importance float64 `json:"importance"`
|
||||
WashCount int `json:"wash_count"`
|
||||
Slug string `json:"slug"`
|
||||
Triggers []string `json:"triggers"`
|
||||
Text string `json:"text"`
|
||||
Importance float64 `json:"importance"`
|
||||
WashCount int `json:"wash_count"`
|
||||
ID int `json:"id"`
|
||||
BodyKnowledge string `json:"body_knowledge"`
|
||||
}
|
||||
|
||||
func scoreNeuron(neuron WarmNeuron, text string) map[string]interface{} {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
// route_alloc.go — the pure budget-allocation slice of route(). PARITY with
|
||||
// warm_router.py's fired-loop decision: the top-scoring relevant neuron
|
||||
// fires its FULL body (relevance outranks budget — what the user asked
|
||||
// about is never compressed); everything after fires its AAAK summary when
|
||||
// available; token cost = max(1, len(body)/4); over-budget neurons are
|
||||
// SKIPPED (not truncated — the next candidate gets the chance).
|
||||
//
|
||||
// The I/O parts of route() (embedding gather, session state, priming,
|
||||
// association walks, use_history writes) stay on the caller side — the
|
||||
// dichotomy: Go owns the reactive loop, the pure decision is here.
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// RouteCandidate — one scored neuron entering the allocation.
|
||||
type RouteCandidate struct {
|
||||
Slug string
|
||||
Body string
|
||||
Summary string // "" = no summary (full body always)
|
||||
Score float64
|
||||
}
|
||||
|
||||
// FiredNeuron — the allocation verdict for one neuron.
|
||||
type FiredNeuron struct {
|
||||
Slug string
|
||||
Body string
|
||||
Score float64
|
||||
Tokens int
|
||||
Compressed bool
|
||||
Primed bool
|
||||
}
|
||||
|
||||
// RouteAllocate — the budget-constrained firing decision.
|
||||
func RouteAllocate(cands []RouteCandidate, maxTokens int) ([]FiredNeuron, int) {
|
||||
sorted := make([]RouteCandidate, len(cands))
|
||||
copy(sorted, cands)
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Score > sorted[j].Score })
|
||||
|
||||
var fired []FiredNeuron
|
||||
used := 0
|
||||
topFired := false
|
||||
for _, n := range sorted {
|
||||
if n.Score <= 0 {
|
||||
continue
|
||||
}
|
||||
body := n.Body
|
||||
if topFired && n.Summary != "" {
|
||||
body = n.Summary
|
||||
}
|
||||
tokens := len(body) / 4
|
||||
if tokens < 1 {
|
||||
tokens = 1
|
||||
}
|
||||
if len(fired) > 0 && used+tokens > maxTokens {
|
||||
continue
|
||||
}
|
||||
fired = append(fired, FiredNeuron{
|
||||
Slug: n.Slug, Body: body, Score: n.Score, Tokens: tokens,
|
||||
Compressed: body != n.Body,
|
||||
})
|
||||
used += tokens
|
||||
topFired = true
|
||||
}
|
||||
return fired, used
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
// scoring.go — the warm-router's deterministic scoring core. PARITY ports
|
||||
// of warm_router.py's _py_score_gate and _py_score_finish (the H3 slice-3
|
||||
// pure oracles). I/O signals (embeddings, SQLite base-level, cache file)
|
||||
// arrive as arguments — the caller gathers them; this code is pure.
|
||||
//
|
||||
// Truth battery: truth_gate.json (live Python oracle, 2026-10-07).
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ScoreGate — pure gate: (fires, total, keywordHits).
|
||||
// Signals 1 (keyword) and 3 (context) are pure; signal 2 (semantic) arrives
|
||||
// as an argument because embeddings are I/O.
|
||||
func ScoreGate(neuron WarmNeuron, lowText string, sessionContext string, semanticScore float64) (bool, float64, int) {
|
||||
keywordHits := 0
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" && strings.Contains(lowText, t) {
|
||||
keywordHits++
|
||||
}
|
||||
}
|
||||
|
||||
contextScore := 0.0
|
||||
if sessionContext != "" {
|
||||
contextLow := strings.ToLower(sessionContext)
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" && strings.Contains(contextLow, t) {
|
||||
contextScore += 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GATED COMBINATION: keywords are the primary gate; semantic is a
|
||||
// secondary boost (>0.3); context provides turn continuity.
|
||||
if keywordHits > 0 {
|
||||
keywordComponent := float64(keywordHits) * 1.0
|
||||
semanticBoost := 0.0
|
||||
if semanticScore > 0.3 {
|
||||
semanticBoost = math.Max(0, semanticScore-0.3) * 0.5
|
||||
}
|
||||
contextComponent := math.Min(contextScore, 1.0)
|
||||
return true, keywordComponent + semanticBoost + contextComponent, keywordHits
|
||||
} else if semanticScore > 0.55 {
|
||||
// No keywords, strong semantic — secondary gate (higher threshold).
|
||||
return true, semanticScore * 0.7, keywordHits
|
||||
}
|
||||
return false, 0.0, keywordHits
|
||||
}
|
||||
|
||||
// ScoreFinish — pure finish: bonuses, penalties, protected boost atop total.
|
||||
// b_i / cachePriority (nil = skip) arrive as arguments: SQLite and the cache
|
||||
// file are I/O — the wrapper only gathers them on the fired path.
|
||||
func ScoreFinish(neuron WarmNeuron, lowText string, total float64, bi *float64, cachePriority *float64, keywordHits int) float64 {
|
||||
score := total + neuron.Importance
|
||||
|
||||
// Signal 4: base-level learning contribution (capped at 0.3).
|
||||
if neuron.ID != 0 && bi != nil {
|
||||
bContribution := math.Max(0, math.Min(*bi*0.1, 0.3))
|
||||
score += bContribution
|
||||
}
|
||||
|
||||
// Signal 5: wash count (drift-with-integrity lifecycle).
|
||||
if neuron.WashCount > 0 {
|
||||
washBoost := math.Min(0.3, math.Log1p(float64(neuron.WashCount))/math.Log1p(20)*0.3)
|
||||
score += washBoost
|
||||
}
|
||||
|
||||
// Signal 6: cache priority.
|
||||
if neuron.ID != 0 && cachePriority != nil {
|
||||
score += math.Min(0.2, *cachePriority*0.1)
|
||||
}
|
||||
|
||||
// Signal 7: body-knowledge relevance.
|
||||
if neuron.BodyKnowledge != "" && lowText != "" {
|
||||
bkLower := strings.ToLower(neuron.BodyKnowledge)
|
||||
qWords := strings.Fields(strings.ToLower(lowText))
|
||||
bkWords := strings.Fields(bkLower)
|
||||
overlap := 0
|
||||
for _, qw := range qWords {
|
||||
for _, bw := range bkWords {
|
||||
if qw == bw {
|
||||
overlap++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if overlap > 0 {
|
||||
score += math.Min(0.15, float64(overlap)*0.03)
|
||||
}
|
||||
}
|
||||
|
||||
// FAN EFFECT PENALTY (ACT-R, Anderson 1974): broad neurons dilute.
|
||||
triggerCount := 0
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" {
|
||||
triggerCount++
|
||||
}
|
||||
}
|
||||
if triggerCount > 10 {
|
||||
fanPenalty := math.Max(0.5, 1.0-float64(triggerCount-10)*0.1)
|
||||
score *= fanPenalty
|
||||
}
|
||||
|
||||
// Length bonus for keyword matches.
|
||||
if keywordHits > 0 {
|
||||
lengthBonus := 0.0
|
||||
for _, t := range neuron.Triggers {
|
||||
if t != "" && len(t) > 6 && strings.Contains(lowText, t) {
|
||||
lengthBonus += 0.25
|
||||
}
|
||||
}
|
||||
score += lengthBonus
|
||||
}
|
||||
|
||||
// Protected neurons get a boost.
|
||||
if ProtectedNeurons[neuron.Slug] {
|
||||
score += ProtectedBoost
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// The protected set + boost — PARITY with warm_router.py:60-61.
|
||||
var ProtectedNeurons = map[string]bool{"persona-alfred": true, "philosophy-sagan-core": true}
|
||||
|
||||
const ProtectedBoost = 4.0
|
||||
@@ -0,0 +1,59 @@
|
||||
[
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "kw_hit",
|
||||
"expected": {
|
||||
"fires": true,
|
||||
"total": 2.0,
|
||||
"kw": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "no_fire",
|
||||
"expected": {
|
||||
"fires": false,
|
||||
"total": 0.0,
|
||||
"kw": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "sem_only",
|
||||
"expected": {
|
||||
"fires": true,
|
||||
"total": 1.25,
|
||||
"kw": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_gate",
|
||||
"name": "ctx",
|
||||
"expected": {
|
||||
"fires": true,
|
||||
"total": 1.3,
|
||||
"kw": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_finish",
|
||||
"name": "fin_full",
|
||||
"expected": {
|
||||
"score": 3.14
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_finish",
|
||||
"name": "fin_min",
|
||||
"expected": {
|
||||
"score": 2.55
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "score_finish",
|
||||
"name": "fan_penalty",
|
||||
"expected": {
|
||||
"score": 1.26
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
// truth_gate_test.go — parity gate for the scoring core. Replays
|
||||
// truth_gate.json (live Python oracle, 2026-10-07) through ScoreGate and
|
||||
// ScoreFinish.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScoreParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_gate.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_gate.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
neuron := WarmNeuron{
|
||||
Slug: "test-neuron", Triggers: []string{"spine", "rebuild"},
|
||||
Importance: 0.8, ID: 42,
|
||||
}
|
||||
for _, c := range cases {
|
||||
switch c.Name {
|
||||
case "kw_hit":
|
||||
fires, total, kw := ScoreGate(neuron, "the spine rebuild verified parity", "", 0.0)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("kw_hit: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "no_fire":
|
||||
fires, total, kw := ScoreGate(neuron, "something unrelated", "", 0.2)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("no_fire: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "sem_only":
|
||||
fires, total, kw := ScoreGate(neuron, "the spinal column rebuild", "", 0.8)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("sem_only: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "ctx":
|
||||
fires, total, kw := ScoreGate(neuron, "spine", "earlier we discussed the spine in detail", 0.0)
|
||||
e := c.Expected
|
||||
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
|
||||
t.Errorf("ctx: got (%v, %v, %v) want %v", fires, total, kw, e)
|
||||
}
|
||||
case "fin_full":
|
||||
bi := 0.5
|
||||
cp := 0.4
|
||||
got := ScoreFinish(neuron, "the spine rebuild verified parity", 2.0, &bi, &cp, 2)
|
||||
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
|
||||
t.Errorf("fin_full: got %v want %v", got, c.Expected["score"])
|
||||
}
|
||||
case "fin_min":
|
||||
got := ScoreFinish(neuron, "the spine rebuild verified parity", 1.5, nil, nil, 1)
|
||||
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
|
||||
t.Errorf("fin_min: got %v want %v", got, c.Expected["score"])
|
||||
}
|
||||
case "fan_penalty":
|
||||
n2 := neuron
|
||||
triggers := []string{}
|
||||
for i := 0; i < 12; i++ {
|
||||
triggers = append(triggers, "t"+string(rune('0'+i%10)))
|
||||
}
|
||||
triggers = append(triggers, "spine")
|
||||
n2.Triggers = triggers
|
||||
got := ScoreFinish(n2, "spine appears", 1.0, nil, nil, 1)
|
||||
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
|
||||
t.Errorf("fan_penalty: got %v want %v", got, c.Expected["score"])
|
||||
}
|
||||
default:
|
||||
t.Errorf("unknown truth case %q", c.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestProtectedBoost — the persona anchors must outscore everything on
|
||||
// their topic (that is the entire point of the boost).
|
||||
func TestProtectedBoost(t *testing.T) {
|
||||
protected := WarmNeuron{Slug: "persona-alfred", Triggers: []string{"composure"}, Importance: 0.8}
|
||||
ordinary := WarmNeuron{Slug: "ordinary", Triggers: []string{"composure"}, Importance: 0.8}
|
||||
p := ScoreFinish(protected, "composure under pressure", 1.0, nil, nil, 1)
|
||||
o := ScoreFinish(ordinary, "composure under pressure", 1.0, nil, nil, 1)
|
||||
if p-o != ProtectedBoost {
|
||||
t.Errorf("protected delta = %v, want %v", p-o, ProtectedBoost)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"fn": "route_alloc",
|
||||
"name": "budget",
|
||||
"max_tokens": 300,
|
||||
"expected": {
|
||||
"fired": [
|
||||
{
|
||||
"slug": "big-primary",
|
||||
"tokens": 100,
|
||||
"compressed": false,
|
||||
"body_len": 400
|
||||
},
|
||||
{
|
||||
"slug": "medium",
|
||||
"tokens": 3,
|
||||
"compressed": true,
|
||||
"body_len": 14
|
||||
},
|
||||
{
|
||||
"slug": "small",
|
||||
"tokens": 12,
|
||||
"compressed": false,
|
||||
"body_len": 50
|
||||
}
|
||||
],
|
||||
"used": 115
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "route_alloc",
|
||||
"name": "generous",
|
||||
"max_tokens": 10000,
|
||||
"expected": {
|
||||
"fired": [
|
||||
{
|
||||
"slug": "big-primary",
|
||||
"tokens": 100,
|
||||
"compressed": false,
|
||||
"body_len": 400
|
||||
},
|
||||
{
|
||||
"slug": "medium",
|
||||
"tokens": 3,
|
||||
"compressed": true,
|
||||
"body_len": 14
|
||||
},
|
||||
{
|
||||
"slug": "small",
|
||||
"tokens": 12,
|
||||
"compressed": false,
|
||||
"body_len": 50
|
||||
}
|
||||
],
|
||||
"used": 115
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
// truth_route_test.go — batch-2 parity gate: the budget-allocation decision
|
||||
// against truth_route.json (live Python walk, 2026-10-07).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRouteAllocParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_route.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_route.json missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Name string `json:"name"`
|
||||
Expected struct {
|
||||
Fired []struct {
|
||||
Slug string `json:"slug"`
|
||||
Tokens int `json:"tokens"`
|
||||
Compressed bool `json:"compressed"`
|
||||
BodyLen int `json:"body_len"`
|
||||
} `json:"fired"`
|
||||
Used int `json:"used"`
|
||||
} `json:"expected"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
cands := []RouteCandidate{
|
||||
{Slug: "big-primary", Body: string(make([]byte, 400)), Summary: "primary summary", Score: 2.5},
|
||||
{Slug: "medium", Body: string(make([]byte, 200)), Summary: "medium summary", Score: 1.2},
|
||||
{Slug: "small", Body: string(make([]byte, 50)), Summary: "", Score: 0.8},
|
||||
}
|
||||
for _, c := range cases {
|
||||
fired, _ := RouteAllocate(cands, c.Expected.Used+1000) // generous budget
|
||||
if len(fired) != len(c.Expected.Fired) {
|
||||
t.Fatalf("%s: fired %d, want %d", c.Name, len(fired), len(c.Expected.Fired))
|
||||
}
|
||||
for i, f := range fired {
|
||||
w := c.Expected.Fired[i]
|
||||
if f.Slug != w.Slug || f.Tokens != w.Tokens || f.Compressed != w.Compressed || len(f.Body) != w.BodyLen {
|
||||
t.Errorf("%s[%d]: got {slug=%s tokens=%d compressed=%v bodyLen=%d} want {slug=%s tokens=%d compressed=%v bodyLen=%d}",
|
||||
c.Name, i, f.Slug, f.Tokens, f.Compressed, len(f.Body), w.Slug, w.Tokens, w.Compressed, w.BodyLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteBudgetSkip — the over-budget behavior: skipped, not truncated,
|
||||
// and the next candidate gets the chance.
|
||||
func TestRouteBudgetSkip(t *testing.T) {
|
||||
cands := []RouteCandidate{
|
||||
{Slug: "primary", Body: string(make([]byte, 400)), Score: 2.5},
|
||||
{Slug: "huge", Body: string(make([]byte, 2000)), Score: 1.5},
|
||||
{Slug: "fits", Body: string(make([]byte, 80)), Score: 1.0},
|
||||
}
|
||||
fired, used := RouteAllocate(cands, 200)
|
||||
slugs := []string{}
|
||||
for _, f := range fired {
|
||||
slugs = append(slugs, f.Slug)
|
||||
}
|
||||
if len(slugs) != 2 || slugs[0] != "primary" || slugs[1] != "fits" {
|
||||
t.Errorf("budget skip: got %v (used %d), want [primary fits]", slugs, used)
|
||||
}
|
||||
if used > 200 {
|
||||
t.Errorf("budget exceeded: used %d > 200", used)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTopNeverCompressed — parity law: the top hit fires its full body even
|
||||
// when a summary exists.
|
||||
func TestTopNeverCompressed(t *testing.T) {
|
||||
cands := []RouteCandidate{
|
||||
{Slug: "top", Body: string(make([]byte, 400)), Summary: "a summary", Score: 2.0},
|
||||
}
|
||||
fired, _ := RouteAllocate(cands, 50) // tiny budget — top still full
|
||||
if len(fired) != 1 || fired[0].Compressed || fired[0].Tokens != 100 {
|
||||
t.Errorf("top compressed under tiny budget: %+v", fired[0])
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.110.0"
|
||||
__version__ = "0.111.0"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.110.0"' in rune_version,
|
||||
'__version__ = "0.111.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
|
||||
|
||||
This is not an interface. This is how I see, speak, and interact with the world.
|
||||
|
||||
Version: v0.110.0
|
||||
Version: v0.111.0
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -1948,7 +1948,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.110.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.111.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
Reference in new issue
Block a user