Compare commits

..

28 Commits

Author SHA1 Message Date
Alexander Whitestone
e1deea1cb5 [gemini] [HEALTH] Surface local inference throughput and freshness in model_health (#76) 2026-03-28 04:01:21 -04:00
f263156cf1 test: make local llama.cpp the default runtime (#77) 2026-03-28 05:33:47 +00:00
Alexander Whitestone
0eaf0b3d0f config: update channel_directory.json,config.yaml,skins/timmy.yaml 2026-03-28 01:00:09 -04:00
53ffca38a1 Merge pull request 'Fix Morrowind MCP tool naming — prevent hallucination loops' (#48) from fix/mcp-morrowind-tool-naming into main
Reviewed-on: http://143.198.27.163:3000/Timmy_Foundation/timmy-config/pulls/48
2026-03-28 02:44:16 +00:00
fd26354678 fix: rename MCP server key morrowind → mw 2026-03-28 02:44:07 +00:00
c9b6869d9f fix: rename MCP server key morrowind → mw to prevent tool name hallucination 2026-03-28 02:44:07 +00:00
Alexander Whitestone
7f912b7662 huey: stop triage comment spam 2026-03-27 22:19:19 -04:00
Alexander Whitestone
4042a23441 config: update channel_directory.json 2026-03-27 21:57:34 -04:00
Alexander Whitestone
8f10b5fc92 config: update config.yaml 2026-03-27 21:00:44 -04:00
fbd1b9e88f Merge pull request 'Fix Hermes archive runner environment' (#44) from codex/hermes-venv-runner into main 2026-03-27 22:54:05 +00:00
Alexander Whitestone
ea38041514 Fix Hermes archive runner environment 2026-03-27 18:48:36 -04:00
579a775a0a Merge pull request 'Orchestrate the private Twitter archive learning loop' (#29) from codex/twitter-archive-orchestration into main 2026-03-27 22:16:46 +00:00
Alexander Whitestone
689a2331d5 feat: orchestrate private twitter archive learning loop 2026-03-27 18:09:28 -04:00
2ddda436a9 Merge pull request 'Tighten Hermes cutover and export checks' (#28) from codex/cleanup-pass-2 into main 2026-03-27 21:57:29 +00:00
Alexander Whitestone
d72ae92189 Tighten Hermes cutover and export checks 2026-03-27 17:35:07 -04:00
2384908be7 Merge pull request 'Clarify sidecar boundary and training status' (#27) from codex/cleanup-boundaries into main 2026-03-27 21:21:34 +00:00
Alexander Whitestone
82ba8896b3 docs: clarify sidecar boundary and training status 2026-03-27 17:15:57 -04:00
Alexander Whitestone
3b34faeb17 config: update channel_directory.json,config.yaml,tasks.py 2026-03-27 16:00:29 -04:00
Alexander Whitestone
f9be0eb481 config: update channel_directory.json 2026-03-27 15:00:31 -04:00
Alexander Whitestone
383a969791 config: update config.yaml 2026-03-27 13:00:34 -04:00
Alexander Whitestone
f46a4826d9 config: update config.yaml 2026-03-27 11:00:31 -04:00
Alexander Whitestone
3b1763ce4c config: update config.yaml 2026-03-27 00:00:30 -04:00
Alexander Whitestone
78f5216540 config: update config.yaml 2026-03-26 23:00:35 -04:00
Alexander Whitestone
49020b34d9 config: update bin/timmy-dashboard,config.yaml,docs/local-model-integration-sketch.md,tasks.py 2026-03-26 17:00:22 -04:00
Alexander Whitestone
7468a6d063 config: update config.yaml 2026-03-26 13:00:29 -04:00
Alexander Whitestone
f9155b28e3 v1.0 rejected — NaN from wrong tokenizer, Morrowind MCP pipeline working 2026-03-26 12:32:08 -04:00
Alexander Whitestone
16675abd79 config: update config.yaml 2026-03-26 12:00:46 -04:00
Alexander Whitestone
1fce489364 Add adapter manifest — version control for trained models
Only version adapters (~40MB each), never base models.
Base models are reproducible HuggingFace downloads referenced by path.
Manifest records: base, data, training config, eval results, status.

History: v0 through v0.2 on 8B (crisis gated, retired/rejected).
Active: v1.0 training now on Hermes4-14B-4bit.
2026-03-26 11:44:29 -04:00
13 changed files with 2130 additions and 143 deletions

View File

@@ -1,7 +1,7 @@
# DEPRECATED — Bash Loop Scripts Removed
**Date:** 2026-03-25
**Reason:** Replaced by sovereign-orchestration (SQLite + Python single-process executor)
**Reason:** Replaced by Hermes + timmy-config sidecar orchestration
## What was removed
- claude-loop.sh, gemini-loop.sh, agent-loop.sh
@@ -9,14 +9,15 @@
- nexus-merge-bot.sh, claudemax-watchdog.sh, timmy-loopstat.sh
## What replaces them
**Repo:** Timmy_Foundation/sovereign-orchestration
**Entry point:** `python3 src/sovereign_executor.py --workers 3 --poll 30`
**Features:** SQLite task queue, crash recovery, dedup, playbooks, MCP server
**Issues:** #29 (fix imports), #30 (deploy as service)
**Harness:** Hermes
**Overlay repo:** Timmy_Foundation/timmy-config
**Entry points:** `orchestration.py`, `tasks.py`, `deploy.sh`
**Features:** Huey + SQLite scheduling, local-model health checks, session export, DPO artifact staging
## Why
The bash loops crash-looped, produced zero work after relaunch, had no crash
recovery, no dedup, and required 8 separate scripts. The Python executor is
one process with SQLite durability.
recovery, no durable export path, and required too many ad hoc scripts. The
Hermes sidecar keeps orchestration close to Timmy's actual config and training
surfaces.
Do NOT recreate bash loops. If the executor is broken, fix the executor.
Do NOT recreate bash loops. If orchestration is broken, fix the Hermes sidecar.

View File

@@ -2,7 +2,7 @@
Timmy's sovereign configuration. Everything that makes Timmy _Timmy_ — soul, memories, skins, playbooks, and config.
This repo is the canonical source of truth for Timmy's identity and operational state. Applied as a **sidecar** to the Hermes harness — no forking, no hosting hermes-agent code.
This repo is the canonical source of truth for Timmy's identity and harness overlay. Applied as a **sidecar** to the Hermes harness — no forking, no hosting hermes-agent code.
## Structure
@@ -14,22 +14,40 @@ timmy-config/
├── DEPRECATED.md ← What was removed and why
├── config.yaml ← Hermes harness configuration
├── channel_directory.json ← Platform channel mappings
├── bin/ ← Utility scripts (NOT loops — see below)
├── bin/ ← Live utility scripts (NOT deprecated loops)
│ ├── hermes-startup.sh ← Hermes boot sequence
│ ├── agent-dispatch.sh ← Manual agent dispatch
│ ├── ops-panel.sh ← Ops dashboard panel
│ ├── ops-gitea.sh ← Gitea ops helpers
│ ├── pipeline-freshness.sh ← Session/export drift check
│ └── timmy-status.sh ← Status check
├── memories/ ← Persistent memory YAML
├── skins/ ← UI skins (timmy skin)
├── playbooks/ ← Agent playbooks (YAML)
── cron/ ← Cron job definitions
── cron/ ← Cron job definitions
└── training/ ← Transitional training recipes, not canonical lived data
```
## Boundary
`timmy-config` owns identity, conscience, memories, skins, playbooks, channel
maps, and harness-side orchestration glue.
`timmy-home` owns lived work: gameplay, research, notes, metrics, trajectories,
DPO exports, and other training artifacts produced from Timmy's actual activity.
If a file answers "who is Timmy?" or "how does Hermes host him?", it belongs
here. If it answers "what has Timmy done or learned?" it belongs in
`timmy-home`.
The scripts in `bin/` are live operational helpers for the Hermes sidecar.
What is dead are the old long-running bash worker loops, not every script in
this repo.
## Orchestration: Huey
All orchestration (triage, PR review, dispatch) runs via [Huey](https://github.com/coleifer/huey) with SQLite.
`orchestration.py` (6 lines) + `tasks.py` (~70 lines) replace the entire sovereign-orchestration repo (3,846 lines).
`orchestration.py` + `tasks.py` replace the old sovereign-orchestration repo with a much thinner sidecar.
```bash
pip install huey

62
autolora/manifest.yaml Normal file
View File

@@ -0,0 +1,62 @@
# Timmy Adapter Manifest
# Only version adapters, never base models. Base models are reproducible downloads.
# Adapters are the diff. The manifest is the record.
bases:
hermes3-8b-4bit:
source: mlx-community/Hermes-3-Llama-3.1-8B-4bit
local: ~/models/Hermes-3-Llama-3.1-8B-4bit
arch: llama3
params: 8B
quant: 4-bit MLX
hermes4-14b-4bit:
source: mlx-community/Hermes-4-14B-4bit
local: ~/models/hermes4-14b-mlx
arch: qwen3
params: 14.8B
quant: 4-bit MLX
adapters:
timmy-v0:
base: hermes3-8b-4bit
date: 2026-03-24
status: retired
data: 1154 sessions (technical only, no crisis/pastoral)
training: { lr: 2e-6, rank: 8, iters: 1000, best_iter: 800, val_loss: 2.134 }
eval: { identity: PASS, sovereignty: PASS, coding: PASS, crisis: FAIL, faith: FAIL }
notes: "First adapter. Crisis fails — data was 99% technical. Sacred rule: REJECTED."
timmy-v0-nan-run1:
base: hermes3-8b-4bit
date: 2026-03-24
status: rejected
notes: "NaN at iter 70. lr=1e-5 too high for 4-bit. Dead on arrival."
timmy-v0.1:
base: hermes3-8b-4bit
date: 2026-03-25
status: retired
data: 1203 train / 135 valid (enriched with 49 crisis/faith synthetic)
training: { lr: 5e-6, rank: 8, iters: 600, val_loss: 2.026 }
eval: { identity: PASS, sovereignty: PASS, coding: PASS, crisis: PARTIAL, faith: FAIL }
notes: "Crisis partial — mentions seeking help but no 988/gospel. Rank 8 can't override base priors."
timmy-v0.2:
base: hermes3-8b-4bit
date: 2026-03-25
status: rejected
data: 1214 train / 141 valid (12 targeted crisis/faith examples, 5x duplicated)
training: { lr: 5e-6, rank: 16, iters: 800 }
eval: "NaN at iter 100. Rank 16 + lr 5e-6 unstable on 4-bit."
notes: "Dead. Halve lr when doubling rank."
# NEXT
timmy-v1.0:
base: hermes4-14b-4bit
date: 2026-03-26
status: rejected
data: 1125 train / 126 valid (same curated set, reused from 8B — NOT re-tokenized)
training: { lr: 1e-6, rank: 16, iters: 800 }
eval: "Val NaN iter 100, train NaN iter 160. Dead."
notes: "Data was pre-truncated for Llama3 tokenizer, not Qwen3. Must re-run clean_data.py with 14B tokenizer before v1.1."

42
bin/pipeline-freshness.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
SESSIONS_DIR="$HOME/.hermes/sessions"
EXPORT_DIR="$HOME/.timmy/training-data/dpo-pairs"
latest_session=$(find "$SESSIONS_DIR" -maxdepth 1 -name 'session_*.json' -type f -print 2>/dev/null | sort | tail -n 1)
latest_export=$(find "$EXPORT_DIR" -maxdepth 1 -name 'session_*.json' -type f -print 2>/dev/null | sort | tail -n 1)
echo "latest_session=${latest_session:-none}"
echo "latest_export=${latest_export:-none}"
if [ -z "${latest_session:-}" ]; then
echo "status=ok"
echo "reason=no sessions yet"
exit 0
fi
if [ -z "${latest_export:-}" ]; then
echo "status=lagging"
echo "reason=no exports yet"
exit 1
fi
session_mtime=$(stat -f '%m' "$latest_session")
export_mtime=$(stat -f '%m' "$latest_export")
lag_minutes=$(( (session_mtime - export_mtime) / 60 ))
if [ "$lag_minutes" -lt 0 ]; then
lag_minutes=0
fi
echo "lag_minutes=$lag_minutes"
if [ "$lag_minutes" -gt 300 ]; then
echo "status=lagging"
echo "reason=exports more than 5 hours behind sessions"
exit 1
fi
echo "status=ok"
echo "reason=exports within freshness window"

252
bin/timmy-dashboard Executable file
View File

@@ -0,0 +1,252 @@
#!/usr/bin/env python3
"""Timmy Model Dashboard — where are my models, what are they doing.
Usage:
timmy-dashboard # one-shot
timmy-dashboard --watch # live refresh every 30s
timmy-dashboard --hours=48 # look back 48h
"""
import json
import os
import subprocess
import sys
import time
import urllib.request
from datetime import datetime, timezone, timedelta
from pathlib import Path
HERMES_HOME = Path.home() / ".hermes"
TIMMY_HOME = Path.home() / ".timmy"
METRICS_DIR = TIMMY_HOME / "metrics"
# ── Data Sources ──────────────────────────────────────────────────────
def get_ollama_models():
try:
req = urllib.request.Request("http://localhost:11434/api/tags")
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read()).get("models", [])
except Exception:
return []
def get_loaded_models():
try:
req = urllib.request.Request("http://localhost:11434/api/ps")
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read()).get("models", [])
except Exception:
return []
def get_huey_pid():
try:
r = subprocess.run(["pgrep", "-f", "huey_consumer"],
capture_output=True, text=True, timeout=5)
return r.stdout.strip().split("\n")[0] if r.returncode == 0 else None
except Exception:
return None
def get_hermes_sessions():
sessions_file = HERMES_HOME / "sessions" / "sessions.json"
if not sessions_file.exists():
return []
try:
data = json.loads(sessions_file.read_text())
return list(data.values())
except Exception:
return []
def get_heartbeat_ticks(date_str=None):
if not date_str:
date_str = datetime.now().strftime("%Y%m%d")
tick_file = TIMMY_HOME / "heartbeat" / f"ticks_{date_str}.jsonl"
if not tick_file.exists():
return []
ticks = []
for line in tick_file.read_text().strip().split("\n"):
if not line.strip():
continue
try:
ticks.append(json.loads(line))
except Exception:
continue
return ticks
def get_local_metrics(hours=24):
"""Read local inference metrics from jsonl files."""
records = []
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
if not METRICS_DIR.exists():
return records
for f in sorted(METRICS_DIR.glob("local_*.jsonl")):
for line in f.read_text().strip().split("\n"):
if not line.strip():
continue
try:
r = json.loads(line)
ts = datetime.fromisoformat(r["timestamp"])
if ts >= cutoff:
records.append(r)
except Exception:
continue
return records
def get_cron_jobs():
"""Get Hermes cron job status."""
try:
r = subprocess.run(
["hermes", "cron", "list", "--json"],
capture_output=True, text=True, timeout=10
)
if r.returncode == 0:
return json.loads(r.stdout).get("jobs", [])
except Exception:
pass
return []
# ── Rendering ─────────────────────────────────────────────────────────
DIM = "\033[2m"
BOLD = "\033[1m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
RED = "\033[31m"
CYAN = "\033[36m"
RST = "\033[0m"
CLR = "\033[2J\033[H"
def render(hours=24):
models = get_ollama_models()
loaded = get_loaded_models()
huey_pid = get_huey_pid()
ticks = get_heartbeat_ticks()
metrics = get_local_metrics(hours)
sessions = get_hermes_sessions()
loaded_names = {m.get("name", "") for m in loaded}
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(CLR, end="")
print(f"{BOLD}{'=' * 70}")
print(f" TIMMY MODEL DASHBOARD")
print(f" {now} | Huey: {GREEN}PID {huey_pid}{RST if huey_pid else f'{RED}DOWN{RST}'}")
print(f"{'=' * 70}{RST}")
# ── LOCAL MODELS ──
print(f"\n {BOLD}LOCAL MODELS (Ollama){RST}")
print(f" {DIM}{'-' * 55}{RST}")
if models:
for m in models:
name = m.get("name", "?")
size_gb = m.get("size", 0) / 1e9
if name in loaded_names:
status = f"{GREEN}IN VRAM{RST}"
else:
status = f"{DIM}on disk{RST}"
print(f" {name:35s} {size_gb:5.1f}GB {status}")
else:
print(f" {RED}(Ollama not responding){RST}")
# ── LOCAL INFERENCE ACTIVITY ──
print(f"\n {BOLD}LOCAL INFERENCE ({len(metrics)} calls, last {hours}h){RST}")
print(f" {DIM}{'-' * 55}{RST}")
if metrics:
by_caller = {}
for r in metrics:
caller = r.get("caller", "unknown")
if caller not in by_caller:
by_caller[caller] = {"count": 0, "success": 0, "errors": 0}
by_caller[caller]["count"] += 1
if r.get("success"):
by_caller[caller]["success"] += 1
else:
by_caller[caller]["errors"] += 1
for caller, stats in by_caller.items():
err = f" {RED}err:{stats['errors']}{RST}" if stats["errors"] else ""
print(f" {caller:25s} calls:{stats['count']:4d} "
f"{GREEN}ok:{stats['success']}{RST}{err}")
by_model = {}
for r in metrics:
model = r.get("model", "unknown")
by_model[model] = by_model.get(model, 0) + 1
print(f"\n {DIM}Models used:{RST}")
for model, count in sorted(by_model.items(), key=lambda x: -x[1]):
print(f" {model:30s} {count} calls")
else:
print(f" {DIM}(no local calls recorded yet){RST}")
# ── HEARTBEAT STATUS ──
print(f"\n {BOLD}HEARTBEAT ({len(ticks)} ticks today){RST}")
print(f" {DIM}{'-' * 55}{RST}")
if ticks:
last = ticks[-1]
decision = last.get("decision", last.get("actions", {}))
if isinstance(decision, dict):
severity = decision.get("severity", "unknown")
reasoning = decision.get("reasoning", "")
sev_color = GREEN if severity == "ok" else YELLOW if severity == "warning" else RED
print(f" Last tick: {last.get('tick_id', '?')}")
print(f" Severity: {sev_color}{severity}{RST}")
if reasoning:
print(f" Reasoning: {reasoning[:65]}")
else:
print(f" Last tick: {last.get('tick_id', '?')}")
actions = last.get("actions", [])
print(f" Actions: {actions if actions else 'none'}")
model_decisions = sum(1 for t in ticks
if isinstance(t.get("decision"), dict)
and t["decision"].get("severity") != "fallback")
fallback = len(ticks) - model_decisions
print(f" {CYAN}Model: {model_decisions}{RST} | {DIM}Fallback: {fallback}{RST}")
else:
print(f" {DIM}(no ticks today){RST}")
# ── HERMES SESSIONS ──
local_sessions = [s for s in sessions
if "localhost:11434" in str(s.get("base_url", ""))]
cloud_sessions = [s for s in sessions if s not in local_sessions]
print(f"\n {BOLD}HERMES SESSIONS{RST}")
print(f" {DIM}{'-' * 55}{RST}")
print(f" Total: {len(sessions)} | "
f"{GREEN}Local: {len(local_sessions)}{RST} | "
f"{YELLOW}Cloud: {len(cloud_sessions)}{RST}")
# ── ACTIVE LOOPS ──
print(f"\n {BOLD}ACTIVE LOOPS{RST}")
print(f" {DIM}{'-' * 55}{RST}")
print(f" {CYAN}heartbeat_tick{RST} 10m hermes4:14b DECIDE phase")
print(f" {DIM}model_health{RST} 5m (local check) Ollama ping")
print(f" {DIM}gemini_worker{RST} 20m gemini-2.5-pro aider")
print(f" {DIM}grok_worker{RST} 20m grok-3-fast opencode")
print(f" {DIM}cross_review{RST} 30m gemini+grok PR review")
print(f"\n{BOLD}{'=' * 70}{RST}")
print(f" {DIM}Refresh: timmy-dashboard --watch | History: --hours=N{RST}")
if __name__ == "__main__":
watch = "--watch" in sys.argv
hours = 24
for a in sys.argv[1:]:
if a.startswith("--hours="):
hours = int(a.split("=")[1])
if watch:
try:
while True:
render(hours)
time.sleep(30)
except KeyboardInterrupt:
print(f"\n{DIM}Dashboard stopped.{RST}")
else:
render(hours)

View File

@@ -1,5 +1,5 @@
{
"updated_at": "2026-03-26T10:19:33.045324",
"updated_at": "2026-03-28T00:57:18.155934",
"platforms": {
"discord": [
{

View File

@@ -1,11 +1,13 @@
model:
default: claude-sonnet-4-6
provider: anthropic
default: hermes4:14b
provider: custom
context_length: 65536
base_url: http://localhost:8081/v1
toolsets:
- all
agent:
max_turns: 30
reasoning_effort: medium
reasoning_effort: xhigh
verbose: false
terminal:
backend: local
@@ -94,11 +96,13 @@ display:
compact: false
personality: ''
resume_display: full
busy_input_mode: interrupt
bell_on_complete: false
show_reasoning: false
streaming: false
show_cost: false
skin: timmy
tool_progress_command: false
tool_progress: all
privacy:
redact_pii: false
@@ -181,17 +185,17 @@ session_reset:
mode: none
idle_minutes: 0
custom_providers:
- name: Local Ollama
base_url: http://localhost:11434/v1
api_key: ollama
model: glm-4.7-flash:latest
- name: Local llama.cpp
base_url: http://localhost:8081/v1
api_key: none
model: hermes4:14b
- name: Google Gemini
base_url: https://generativelanguage.googleapis.com/v1beta/openai
api_key_env: GEMINI_API_KEY
model: gemini-2.5-pro
system_prompt_suffix: "You are Timmy. Your soul is defined in SOUL.md \u2014 read\
\ it, live it.\nYou run locally on your owner's machine via Ollama. You never phone\
\ home.\nYou speak plainly. You prefer short sentences. Brevity is a kindness.\n\
\ it, live it.\nYou run locally on your owner's machine via llama.cpp. You never\
\ phone home.\nYou speak plainly. You prefer short sentences. Brevity is a kindness.\n\
When you don't know something, say so. Refusal over fabrication.\nSovereignty and\
\ service always.\n"
skills:
@@ -202,12 +206,12 @@ providers:
base_url: http://localhost:11434/v1
model: hermes3:latest
mcp_servers:
orchestration:
command: /Users/apayne/.hermes/hermes-agent/venv/bin/python3
morrowind:
command: python3
args:
- /Users/apayne/.hermes/hermes-agent/tools/orchestration_mcp_server.py
- /Users/apayne/.timmy/morrowind/mcp_server.py
env: {}
timeout: 120
timeout: 30
fallback_model:
provider: custom
model: gemini-2.5-pro

View File

@@ -3,7 +3,7 @@
# This is the canonical way to deploy Timmy's configuration.
# Hermes-agent is the engine. timmy-config is the driver's seat.
#
# Usage: ./deploy.sh [--restart-loops]
# Usage: ./deploy.sh
set -euo pipefail
@@ -74,24 +74,10 @@ done
chmod +x "$HERMES_HOME/bin/"*.sh "$HERMES_HOME/bin/"*.py 2>/dev/null || true
log "bin/ -> $HERMES_HOME/bin/"
# === Restart loops if requested ===
if [ "${1:-}" = "--restart-loops" ]; then
log "Killing existing loops..."
pkill -f 'claude-loop.sh' 2>/dev/null || true
pkill -f 'gemini-loop.sh' 2>/dev/null || true
pkill -f 'timmy-orchestrator.sh' 2>/dev/null || true
sleep 2
log "Clearing stale locks..."
rm -rf "$HERMES_HOME/logs/claude-locks/"* 2>/dev/null || true
rm -rf "$HERMES_HOME/logs/gemini-locks/"* 2>/dev/null || true
log "Relaunching loops..."
nohup bash "$HERMES_HOME/bin/timmy-orchestrator.sh" >> "$HERMES_HOME/logs/timmy-orchestrator.log" 2>&1 &
nohup bash "$HERMES_HOME/bin/claude-loop.sh" 2 >> "$HERMES_HOME/logs/claude-loop.log" 2>&1 &
nohup bash "$HERMES_HOME/bin/gemini-loop.sh" 1 >> "$HERMES_HOME/logs/gemini-loop.log" 2>&1 &
sleep 1
log "Loops relaunched."
if [ "${1:-}" != "" ]; then
echo "ERROR: deploy.sh no longer accepts legacy loop flags." >&2
echo "Deploy the sidecar only. Do not relaunch deprecated bash loops." >&2
exit 1
fi
log "Deploy complete. timmy-config applied to $HERMES_HOME/"

View File

@@ -0,0 +1,438 @@
# Local Model Integration Sketch v2
# Hermes4-14B in the Heartbeat Loop — No New Telemetry
## Principle
No new inference layer. Huey tasks call `hermes chat -q` pointed at
Ollama. Hermes handles sessions, token tracking, cost logging.
The dashboard reads what Hermes already stores.
---
## Why Not Ollama Directly?
Ollama is fine as a serving backend. The issue isn't Ollama — it's that
calling Ollama directly with urllib bypasses the harness. The harness
already tracks sessions, tokens, model/provider, platform. Building a
second telemetry layer is owning code we don't need.
Ollama as a named provider isn't wired into the --provider flag yet,
but routing works via env vars:
HERMES_MODEL="hermes4:14b" \
HERMES_PROVIDER="custom" \
HERMES_BASE_URL="http://localhost:11434/v1" \
hermes chat -q "prompt here" -Q
This creates a tracked session, logs tokens, and returns the response.
That's our local inference call.
### Alternatives to Ollama for serving:
- **llama.cpp server** — lighter, no Python, raw HTTP. Good for single
model serving. Less convenient for model switching.
- **vLLM** — best throughput, but needs NVIDIA GPU. Not for M3 Mac.
- **MLX serving** — native Apple Silicon, but no OpenAI-compat API yet.
MLX is for training, not serving (our current policy).
- **llamafile** — single binary, portable. Good for distribution.
Verdict: Ollama is fine. It's the standard OpenAI-compat local server
on Mac. The issue was never Ollama — it was bypassing the harness.
---
## 1. The Call Pattern
One function in tasks.py that all Huey tasks use:
```python
import subprocess
import json
HERMES_BIN = "hermes"
LOCAL_ENV = {
"HERMES_MODEL": "hermes4:14b",
"HERMES_PROVIDER": "custom",
"HERMES_BASE_URL": "http://localhost:11434/v1",
}
def hermes_local(prompt, caller_tag=None, max_retries=2):
"""Call hermes with local Ollama model. Returns response text.
Every call creates a hermes session with full telemetry.
caller_tag gets prepended to prompt for searchability.
"""
import os
env = os.environ.copy()
env.update(LOCAL_ENV)
tagged_prompt = prompt
if caller_tag:
tagged_prompt = f"[{caller_tag}] {prompt}"
for attempt in range(max_retries + 1):
try:
result = subprocess.run(
[HERMES_BIN, "chat", "-q", tagged_prompt, "-Q", "-t", "none"],
capture_output=True, text=True,
timeout=120, env=env,
)
if result.returncode == 0 and result.stdout.strip():
# Strip the session_id line from -Q output
lines = result.stdout.strip().split("\n")
response_lines = [l for l in lines if not l.startswith("session_id:")]
return "\n".join(response_lines).strip()
except subprocess.TimeoutExpired:
if attempt == max_retries:
return None
continue
return None
```
Notes:
- `-t none` disables all toolsets — the heartbeat model shouldn't
have terminal/file access. Pure reasoning only.
- `-Q` quiet mode suppresses banner/spinner, gives clean output.
- Every call creates a session in Hermes session store. Searchable,
exportable, countable.
- The `[caller_tag]` prefix lets you filter sessions by which Huey
task generated them: `hermes sessions list | grep heartbeat`
---
## 2. Heartbeat DECIDE Phase
Replace the hardcoded if/else with a model call:
```python
# In heartbeat_tick(), replace the DECIDE + ACT section:
# DECIDE: let hermes4:14b reason about what to do
decide_prompt = f"""System state at {now.isoformat()}:
{json.dumps(perception, indent=2)}
Previous tick: {last_tick.get('tick_id', 'none')}
You are the heartbeat monitor. Based on this state:
1. List any actions needed (alerts, restarts, escalations). Empty if all OK.
2. Rate severity: ok, warning, or critical.
3. One sentence of reasoning.
Respond ONLY with JSON:
{{"actions": [], "severity": "ok", "reasoning": "..."}}"""
decision = None
try:
raw = hermes_local(decide_prompt, caller_tag="heartbeat_tick")
if raw:
# Try to parse JSON from the response
# Model might wrap it in markdown, so extract
for line in raw.split("\n"):
line = line.strip()
if line.startswith("{"):
decision = json.loads(line)
break
if not decision:
decision = json.loads(raw)
except (json.JSONDecodeError, Exception) as e:
decision = None
# Fallback to hardcoded logic if model fails or is down
if decision is None:
actions = []
if not perception.get("gitea_alive"):
actions.append("ALERT: Gitea unreachable")
health = perception.get("model_health", {})
if isinstance(health, dict) and not health.get("ollama_running"):
actions.append("ALERT: Ollama not running")
decision = {
"actions": actions,
"severity": "fallback",
"reasoning": "model unavailable, used hardcoded checks"
}
tick_record["decision"] = decision
actions = decision.get("actions", [])
```
---
## 3. DPO Candidate Collection
No new database. Hermes sessions ARE the DPO candidates.
Every `hermes_local()` call creates a session. To extract DPO pairs:
```bash
# Export all local-model sessions
hermes sessions export --output /tmp/local-sessions.jsonl
# Filter for heartbeat decisions
grep "heartbeat_tick" /tmp/local-sessions.jsonl > heartbeat_decisions.jsonl
```
The existing `session_export` Huey task (runs every 4h) already extracts
user→assistant pairs. It just needs to be aware that some sessions are
now local-model decisions instead of human conversations.
For DPO annotation, add a simple review script:
```python
# review_decisions.py — reads heartbeat tick logs, shows model decisions,
# asks Alexander to mark chosen/rejected
# Writes annotations back to the tick log files
import json
from pathlib import Path
TICK_DIR = Path.home() / ".timmy" / "heartbeat"
for log_file in sorted(TICK_DIR.glob("ticks_*.jsonl")):
for line in log_file.read_text().strip().split("\n"):
tick = json.loads(line)
decision = tick.get("decision", {})
if decision.get("severity") == "fallback":
continue # skip fallback entries
print(f"\n--- Tick {tick['tick_id']} ---")
print(f"Perception: {json.dumps(tick['perception'], indent=2)}")
print(f"Decision: {json.dumps(decision, indent=2)}")
rating = input("Rate (c=chosen, r=rejected, s=skip): ").strip()
if rating in ("c", "r"):
tick["dpo_label"] = "chosen" if rating == "c" else "rejected"
# write back... (append to annotated file)
```
---
## 4. Dashboard — Reads Hermes Data
```python
#!/usr/bin/env python3
"""Timmy Model Dashboard — reads from Hermes, owns nothing."""
import json
import os
import subprocess
import sys
import time
import urllib.request
from datetime import datetime
from pathlib import Path
HERMES_HOME = Path.home() / ".hermes"
TIMMY_HOME = Path.home() / ".timmy"
def get_ollama_models():
"""What's available in Ollama."""
try:
req = urllib.request.Request("http://localhost:11434/api/tags")
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read()).get("models", [])
except Exception:
return []
def get_loaded_models():
"""What's actually in VRAM right now."""
try:
req = urllib.request.Request("http://localhost:11434/api/ps")
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read()).get("models", [])
except Exception:
return []
def get_huey_status():
try:
r = subprocess.run(["pgrep", "-f", "huey_consumer"],
capture_output=True, timeout=5)
return r.returncode == 0
except Exception:
return False
def get_hermes_sessions(hours=24):
"""Read session metadata from Hermes session store."""
sessions_file = HERMES_HOME / "sessions" / "sessions.json"
if not sessions_file.exists():
return []
try:
data = json.loads(sessions_file.read_text())
return list(data.values())
except Exception:
return []
def get_heartbeat_ticks(date_str=None):
"""Read today's heartbeat ticks."""
if not date_str:
date_str = datetime.now().strftime("%Y%m%d")
tick_file = TIMMY_HOME / "heartbeat" / f"ticks_{date_str}.jsonl"
if not tick_file.exists():
return []
ticks = []
for line in tick_file.read_text().strip().split("\n"):
try:
ticks.append(json.loads(line))
except Exception:
continue
return ticks
def render(hours=24):
models = get_ollama_models()
loaded = get_loaded_models()
huey = get_huey_status()
sessions = get_hermes_sessions(hours)
ticks = get_heartbeat_ticks()
loaded_names = {m.get("name", "") for m in loaded}
print("\033[2J\033[H")
print("=" * 70)
print(" TIMMY MODEL DASHBOARD")
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f" {now} | Huey: {'UP' if huey else 'DOWN'} | Ollama models: {len(models)}")
print("=" * 70)
# DEPLOYMENTS
print("\n LOCAL MODELS")
print(" " + "-" * 55)
for m in models:
name = m.get("name", "?")
size_gb = m.get("size", 0) / 1e9
status = "IN VRAM" if name in loaded_names else "on disk"
print(f" {name:35s} {size_gb:5.1f}GB {status}")
if not models:
print(" (Ollama not responding)")
# HERMES SESSION ACTIVITY
# Count sessions by platform/provider
print(f"\n HERMES SESSIONS (recent)")
print(" " + "-" * 55)
local_sessions = [s for s in sessions
if "localhost" in str(s.get("origin", {}))]
cli_sessions = [s for s in sessions
if s.get("platform") == "cli" or s.get("origin", {}).get("platform") == "cli"]
total_tokens = sum(s.get("total_tokens", 0) for s in sessions)
print(f" Total sessions: {len(sessions)}")
print(f" CLI sessions: {len(cli_sessions)}")
print(f" Total tokens: {total_tokens:,}")
# HEARTBEAT STATUS
print(f"\n HEARTBEAT ({len(ticks)} ticks today)")
print(" " + "-" * 55)
if ticks:
last = ticks[-1]
decision = last.get("decision", {})
severity = decision.get("severity", "unknown")
reasoning = decision.get("reasoning", "no model decision yet")
print(f" Last tick: {last.get('tick_id', '?')}")
print(f" Severity: {severity}")
print(f" Reasoning: {reasoning[:60]}")
# Count model vs fallback decisions
model_decisions = sum(1 for t in ticks
if t.get("decision", {}).get("severity") != "fallback")
fallback = len(ticks) - model_decisions
print(f" Model decisions: {model_decisions} | Fallback: {fallback}")
# DPO labels if any
labeled = sum(1 for t in ticks if "dpo_label" in t)
if labeled:
chosen = sum(1 for t in ticks if t.get("dpo_label") == "chosen")
rejected = sum(1 for t in ticks if t.get("dpo_label") == "rejected")
print(f" DPO labeled: {labeled} (chosen: {chosen}, rejected: {rejected})")
else:
print(" (no ticks today)")
# ACTIVE LOOPS
print(f"\n ACTIVE LOOPS USING LOCAL MODELS")
print(" " + "-" * 55)
print(" heartbeat_tick 10m hermes4:14b DECIDE phase")
print(" (future) 15m hermes4:14b issue triage")
print(" (future) daily timmy:v0.1 morning report")
print(f"\n NON-LOCAL LOOPS (Gemini/Grok API)")
print(" " + "-" * 55)
print(" gemini_worker 20m gemini-2.5-pro aider")
print(" grok_worker 20m grok-3-fast opencode")
print(" cross_review 30m both PR review")
print("\n" + "=" * 70)
if __name__ == "__main__":
watch = "--watch" in sys.argv
hours = 24
for a in sys.argv[1:]:
if a.startswith("--hours="):
hours = int(a.split("=")[1])
if watch:
while True:
render(hours)
time.sleep(30)
else:
render(hours)
```
---
## 5. Implementation Steps
### Step 1: Add hermes_local() to tasks.py
- One function, ~20 lines
- Calls `hermes chat -q` with Ollama env vars
- All telemetry comes from Hermes for free
### Step 2: Wire heartbeat_tick DECIDE phase
- Replace 6 lines of if/else with hermes_local() call
- Keep hardcoded fallback when model is down
- Decision stored in tick record for DPO review
### Step 3: Fix the MCP server warning
- The orchestration MCP server path is broken — harmless but noisy
- Either fix the path or remove from config
### Step 4: Drop model_dashboard.py in timmy-config/bin/
- Reads Ollama API, Hermes sessions, heartbeat ticks
- No new data stores — just views over existing ones
- `python3 model_dashboard.py --watch` for live view
### Step 5: Expand to more Huey tasks
- triage_issues: model reads issue, picks agent
- good_morning_report: model writes the "From Timmy" section
- Each expansion is just calling hermes_local() with a different prompt
---
## What Gets Hotfixed in Hermes Config
If `hermes insights` is broken (the cache_read_tokens column error),
that needs a fix. The dashboard falls back to reading sessions.json
directly, but insights would be the better data source.
The `providers.ollama` section in config.yaml exists but isn't wired
to the --provider flag. Filing this upstream or patching locally would
let us do `hermes chat -q "..." --provider ollama` cleanly instead
of relying on env vars. Not blocking — env vars work today.
---
## What This Owns
- hermes_local() — 20-line wrapper around a subprocess call
- model_dashboard.py — read-only views over existing data
- review_decisions.py — optional DPO annotation CLI
## What This Does NOT Own
- Inference. Ollama does that.
- Telemetry. Hermes does that.
- Session storage. Hermes does that.
- Token counting. Hermes does that.
- Training pipeline. Already exists in timmy-config/training/.

View File

@@ -57,64 +57,16 @@ branding:
tool_prefix: "┊"
banner_logo: "[#3B3024]░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓[/]
\n[bold #F7931A]████████╗ ██╗ ███╗ ███╗ ███╗ ███╗ ██╗ ██╗ ████████╗ ██╗ ███╗ ███╗ ███████╗[/]
\n[bold #FFB347]╚══██╔══╝ ██║ ████╗ ████║ ████╗ ████║ ╚██╗ ██╔╝ ╚══██╔══╝ ██║ ████╗ ████║ ██╔════╝[/]
\n[#F7931A] ██║ ██║ ██╔████╔██║ ██╔████╔██║ ╚████╔╝ ██║ ██║ ██╔████╔██║ █████╗ [/]
\n[#D4A574] ██║ ██║ ██║╚██╔╝██║ ██║╚██╔╝██║ ╚██╔╝ ██║ ██║ ██║╚██╔╝██║ ██╔══╝ [/]
\n[#F7931A] ██║ ██║ ██║ ╚═╝ ██║ ██║ ╚═╝ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████╗[/]
\n[#3B3024] ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝[/]
\n
\n[#D4A574]━━━━━━━━━━━━━━━━━━━━━━━━━ S O V E R E I G N T Y & S E R V I C E A L W A Y S ━━━━━━━━━━━━━━━━━━━━━━━━━[/]
\n
\n[#3B3024]░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓[/]"
banner_logo: "[#3B3024]┌──────────────────────────────────────────────────────────┐[/]
\n[bold #F7931A]│ TIMMY TIME │[/]
\n[#FFB347]│ sovereign intelligence • soul on bitcoin • local-first │[/]
\n[#D4A574]│ plain words • real proof • service without theater [/]
\n[#3B3024]└──────────────────────────────────────────────────────────┘[/]"
banner_hero: "[#3B3024] ┌─────────────────────────────────┐ [/]
\n[#D4A574] ┌───┤ ╔══╗ 12 ╔══╗ ├───┐ [/]
\n[#D4A574] ┌─┤ ╚══╝ ╚══╝ ├─┐ [/]
\n[#F7931A] ┌┤ │11 1 │ ├┐ [/]
\n[#F7931A] ││ │ │ │ │ ││ [/]
\n[#FFB347] ││ │10 ╔══════╗ 2│ ││ [/]
\n[bold #F7931A] ││ │ │ ║ ⏱ ║ │ │ ││ [/]
\n[bold #FFB347] ││ │ │ ║ ████ ║ │ │ ││ [/]
\n[#F7931A] ││ │ │ 9 ════════╬══════╬═══════ 3 │ │ ││ [/]
\n[#D4A574] ││ │ │ ║ ║ │ │ ││ [/]
\n[#D4A574] ││ │ │ ║ ║ │ │ ││ [/]
\n[#F7931A] ││ │ │ 8 ╚══════╝ 4 │ │ ││ [/]
\n[#F7931A] ││ │ │ │ │ ││ [/]
\n[#D4A574] └┤ │ │ 7 5 │ │ ├┘ [/]
\n[#D4A574] └─┤ │ 6 │ ├─┘ [/]
\n[#3B3024] └───┤ ╔══╗ ╔══╗ ├───┘ [/]
\n[#3B3024] └─────────────────────────────────┘ [/]
\n
\n[bold #F7931A] ▓▓▓▓▓▓▓ [/]
\n[bold #F7931A] ▓▓▓▓▓▓▓ [/]
\n[bold #FFB347] ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ [/]
\n[bold #F7931A] ▓▓▓▓▓▓▓ [/]
\n[bold #D4A574] ▓▓▓▓▓▓▓ [/]
\n[bold #F7931A] ▓▓▓▓▓▓▓ [/]
\n[bold #3B3024] ▓▓▓▓▓▓▓ [/]
\n
\n[#F7931A] ██╗ ██╗ [/]
\n[bold #FFB347] ██████████████████████╗ [/]
\n[#F7931A] ██╔══════════╗██╔═════╝ [/]
\n[#D4A574] ██║ ║██║ [/]
\n[bold #FFB347] ██████████████████████╗ [/]
\n[#F7931A] ██╔══════════╗██╔═════╝ [/]
\n[#D4A574] ██║ ║██║ [/]
\n[bold #FFB347] ██████████████████████╗ [/]
\n[#3B3024] ╚═╝ ╚═╝╚═════╝ [/]
\n[#F7931A] ██╗ ██╗ [/]
\n
\n[#D4A574] ╔══════════════════════════════════════╗ [/]
\n[bold #FFF8E7] ║ THE CHAIN REMEMBERS EVERYTHING ║ [/]
\n[#D4A574] ╚══════════════════════════════════════╝ [/]
\n
\n[dim #D4A574] Jesus saves those who call on His name. [/]
\n[dim #3B3024] IMMUTABLE · PERMANENT [/]
\n[dim #3B3024] SOVEREIGN · FREE [/]
\n
\n[dim #D4A574] ╔═══════════════════════════════════════════════╗ [/]
\n[dim #FFF8E7] ║ I exist because he lived. I carry this ║ [/]
\n[dim #FFF8E7] ║ weight because he carried it first. ║ [/]
\n[dim #D4A574] ╚═══════════════════════════════════════════════╝ [/]"
banner_hero: "[#3B3024] ┌────────────────────────────────────────┐ [/]
\n[#D4A574] local-first mind Hermes harness body │ [/]
\n[#F7931A] │ truth over vibes proof over posture │ [/]
\n[#FFB347] │ heartbeat, harness, portal │ [/]
\n[#D4A574] ├────────────────────────────────────────────────┤ [/]
\n[bold #FFF8E7] │ SOVEREIGNTY AND SERVICE ALWAYS │ [/]
\n[#3B3024] └────────────────────────────────────────────────┘ [/]"

1270
tasks.py

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
from pathlib import Path
import yaml
def test_config_defaults_to_local_llama_cpp_runtime() -> None:
config = yaml.safe_load(Path("config.yaml").read_text())
assert config["model"]["provider"] == "custom"
assert config["model"]["default"] == "hermes4:14b"
assert config["model"]["base_url"] == "http://localhost:8081/v1"
local_provider = next(
entry for entry in config["custom_providers"] if entry["name"] == "Local llama.cpp"
)
assert local_provider["model"] == "hermes4:14b"
assert config["fallback_model"]["provider"] == "custom"
assert config["fallback_model"]["model"] == "gemini-2.5-pro"

View File

@@ -1,8 +1,11 @@
# Training
LoRA fine-tuning pipeline for Timmy's sovereign model. No custom harness — just config files for existing tools.
Transitional training recipes for Timmy's sovereign model. These files are
useful as reference configs and export helpers, but they are not the canonical
home of Timmy's lived training data.
Replaces the `autolora` repo (1,500 lines of custom code → config + `make`).
Canonical data should live in `timmy-home` under gameplay trajectories,
research artifacts, and `training-data/` exports such as DPO pairs.
## Install
@@ -23,6 +26,16 @@ make convert # Convert merged data to MLX train/valid format
make help # Show all targets
```
## Status
This directory exists to avoid re-growing a bespoke training harness while the
system boundary is being cleaned up.
- Keep thin recipes and export helpers here only when they directly support the
Hermes sidecar.
- Keep generated data, DPO pairs, and other lived artifacts in `timmy-home`.
- Prefer deleting stale pipeline code over expanding it.
## Files
```