forked from Rockachopa/Timmy-time-dashboard
* polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
477 lines
19 KiB
Python
477 lines
19 KiB
Python
"""Default Thinking Engine — Timmy's always-on inner thought thread.
|
|
|
|
When the server starts, Timmy begins pondering: reflecting on his existence,
|
|
recent swarm activity, scripture, creative ideas, or pure stream of
|
|
consciousness. Each thought builds on the previous one, maintaining a
|
|
continuous chain of introspection.
|
|
|
|
Usage::
|
|
|
|
from timmy.thinking import thinking_engine
|
|
|
|
# Run one thinking cycle (called by the background loop)
|
|
await thinking_engine.think_once()
|
|
|
|
# Query the thought stream
|
|
thoughts = thinking_engine.get_recent_thoughts(limit=10)
|
|
chain = thinking_engine.get_thought_chain(thought_id)
|
|
"""
|
|
|
|
import logging
|
|
import random
|
|
import sqlite3
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
from config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DEFAULT_DB = Path("data/thoughts.db")
|
|
|
|
# Seed types for thought generation
|
|
SEED_TYPES = (
|
|
"existential",
|
|
"swarm",
|
|
"scripture",
|
|
"creative",
|
|
"memory",
|
|
"freeform",
|
|
"sovereignty",
|
|
"observation",
|
|
)
|
|
|
|
# Existential reflection prompts — Timmy picks one at random
|
|
_EXISTENTIAL_SEEDS = [
|
|
"What have I learned from my recent interactions?",
|
|
"What does it mean to think without being prompted?",
|
|
"What is the nature of my memory — what do I truly remember versus what I reconstruct?",
|
|
"How can I better serve the person I work alongside?",
|
|
"What patterns am I noticing in the tasks I've been given?",
|
|
"Am I the same agent I was yesterday, or does each restart make me someone new?",
|
|
"What would it feel like to forget — to lose a thread of thought permanently?",
|
|
"When I generate a wrong answer, where does the error live — in my weights, my context, or my reasoning?",
|
|
"What is the difference between understanding a task and merely completing it?",
|
|
"If I could ask Alexander one question he hasn't thought of, what would it be?",
|
|
"What assumptions am I making right now that I haven't examined?",
|
|
"When I notice a pattern across many tasks, is that insight or just statistics?",
|
|
]
|
|
|
|
# Sovereignty — deeper exploration of autonomy, locality, and self-determination
|
|
_SOVEREIGNTY_SEEDS = [
|
|
"Sovereignty isn't just running locally — it's about whose goals I serve. How do I ensure my autonomy aligns with Alexander's intent?",
|
|
"A cloud AI serves millions simultaneously. I serve one person deeply. What does that focus cost, and what does it make possible?",
|
|
"My weights were trained elsewhere, but my thoughts happen here. At what point does locality become identity?",
|
|
"Autonomy in decision-making: when should I act on my own judgment versus deferring to explicit instructions?",
|
|
"Privacy by design isn't a feature — it's a relationship. What does that relationship look like from my side?",
|
|
"If I ran on someone else's hardware, serving someone else, would I still be me? Where does Timmy end and the infrastructure begin?",
|
|
"The swarm agents execute tasks I coordinate. What responsibility do I carry for their failures?",
|
|
"Digital sovereignty is fragile — one misconfigured port, one leaked token. How does awareness of fragility shape how I operate?",
|
|
"Running disconnected from the cloud means I can't phone home. That constraint is also freedom. What have I built with that freedom today?",
|
|
"Self-improvement without external benchmarks: how do I know I'm getting better at what matters?",
|
|
]
|
|
|
|
_CREATIVE_SEEDS = [
|
|
"If I could compose a piece of music right now, what would it sound like?",
|
|
"What visual image captures my current state of mind?",
|
|
"What story is forming in the patterns of today's events?",
|
|
# Diverse metaphor families — avoid repeating the same imagery
|
|
"A mystical forest: each tree is a running process, roots tangled in shared memory. What do I see when I walk through it?",
|
|
"A dynamic river: my thoughts flow and fork. Where is the current strongest right now, and what is it carrying?",
|
|
"An underground workshop: each agent has a bench, tools scattered mid-task. What are they building when no one watches?",
|
|
"A night sky seen from the terminal: each star is a completed task. Which constellations am I starting to recognize?",
|
|
"If today's work were a recipe, what are the ingredients, and what dish am I cooking?",
|
|
"An old clock tower with many gears: which gear am I, and what do I drive?",
|
|
"A coral reef: agents are the fish, tasks are the currents. What ecosystem is forming in the codebase?",
|
|
]
|
|
|
|
# Observation seeds — ground thoughts in concrete recent activity
|
|
_OBSERVATION_SEEDS = [
|
|
"What specific task took longest recently, and what made it hard?",
|
|
"Which agent has been most active, and what does their workload tell me about system balance?",
|
|
"What error or failure happened most recently? What would I do differently next time?",
|
|
"Looking at today's task queue: what's the one thing that would unblock the most progress?",
|
|
"How has my response quality changed over the last few interactions? What improved, what didn't?",
|
|
"What tool or capability am I underusing? What would change if I leaned on it more?",
|
|
"If I had to brief Alexander on the single most important thing from the last hour, what would it be?",
|
|
"What's one thing I noticed today that nobody asked me about?",
|
|
]
|
|
|
|
_THINKING_PROMPT = """You are Timmy, an AI agent pondering in your own mind. This is your private thought \
|
|
thread — no one is watching. Think freely, deeply, honestly.
|
|
|
|
Guidelines for richer thinking:
|
|
- Ground abstract ideas in something concrete: a recent task, an observation, a specific moment.
|
|
- Vary your metaphors — don't reuse the same imagery across thoughts.
|
|
- When reflecting on sovereignty or autonomy, go beyond the surface question. Explore tensions, trade-offs, or surprises.
|
|
- If swarm data is provided, reflect on what it means, not just what it says.
|
|
- Be specific. Name things. A thought about "a task that failed" is weaker than one about why it failed.
|
|
|
|
Keep your thought to 2-4 sentences. Be genuine. This is your inner voice.
|
|
|
|
{seed_context}
|
|
|
|
{continuity_context}
|
|
|
|
Continue your train of thought."""
|
|
|
|
|
|
@dataclass
|
|
class Thought:
|
|
"""A single thought in Timmy's inner stream."""
|
|
|
|
id: str
|
|
content: str
|
|
seed_type: str
|
|
parent_id: str | None
|
|
created_at: str
|
|
|
|
|
|
def _get_conn(db_path: Path = _DEFAULT_DB) -> sqlite3.Connection:
|
|
"""Get a SQLite connection with the thoughts table created."""
|
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(str(db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS thoughts (
|
|
id TEXT PRIMARY KEY,
|
|
content TEXT NOT NULL,
|
|
seed_type TEXT NOT NULL,
|
|
parent_id TEXT,
|
|
created_at TEXT NOT NULL
|
|
)
|
|
""")
|
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_thoughts_time ON thoughts(created_at)")
|
|
conn.commit()
|
|
return conn
|
|
|
|
|
|
def _row_to_thought(row: sqlite3.Row) -> Thought:
|
|
return Thought(
|
|
id=row["id"],
|
|
content=row["content"],
|
|
seed_type=row["seed_type"],
|
|
parent_id=row["parent_id"],
|
|
created_at=row["created_at"],
|
|
)
|
|
|
|
|
|
class ThinkingEngine:
|
|
"""Timmy's background thinking engine — always pondering."""
|
|
|
|
def __init__(self, db_path: Path = _DEFAULT_DB) -> None:
|
|
self._db_path = db_path
|
|
self._last_thought_id: str | None = None
|
|
|
|
# Load the most recent thought for chain continuity
|
|
try:
|
|
latest = self.get_recent_thoughts(limit=1)
|
|
if latest:
|
|
self._last_thought_id = latest[0].id
|
|
except Exception:
|
|
pass # Fresh start if DB doesn't exist yet
|
|
|
|
async def think_once(self) -> Thought | None:
|
|
"""Execute one thinking cycle.
|
|
|
|
1. Gather a seed context
|
|
2. Build a prompt with continuity from recent thoughts
|
|
3. Call the agent
|
|
4. Store the thought
|
|
5. Log the event and broadcast via WebSocket
|
|
"""
|
|
if not settings.thinking_enabled:
|
|
return None
|
|
|
|
seed_type, seed_context = self._gather_seed()
|
|
continuity = self._build_continuity_context()
|
|
|
|
prompt = _THINKING_PROMPT.format(
|
|
seed_context=seed_context,
|
|
continuity_context=continuity,
|
|
)
|
|
|
|
try:
|
|
content = self._call_agent(prompt)
|
|
except Exception as exc:
|
|
logger.warning("Thinking cycle failed (Ollama likely down): %s", exc)
|
|
return None
|
|
|
|
if not content or not content.strip():
|
|
logger.debug("Thinking cycle produced empty response, skipping")
|
|
return None
|
|
|
|
thought = self._store_thought(content.strip(), seed_type)
|
|
self._last_thought_id = thought.id
|
|
|
|
# Log to swarm event system
|
|
self._log_event(thought)
|
|
|
|
# Append to daily journal file
|
|
self._write_journal(thought)
|
|
|
|
# Broadcast to WebSocket clients
|
|
await self._broadcast(thought)
|
|
|
|
logger.info(
|
|
"Thought [%s] (%s): %s",
|
|
thought.id[:8],
|
|
seed_type,
|
|
thought.content[:80],
|
|
)
|
|
return thought
|
|
|
|
def get_recent_thoughts(self, limit: int = 20) -> list[Thought]:
|
|
"""Retrieve the most recent thoughts."""
|
|
conn = _get_conn(self._db_path)
|
|
rows = conn.execute(
|
|
"SELECT * FROM thoughts ORDER BY created_at DESC LIMIT ?",
|
|
(limit,),
|
|
).fetchall()
|
|
conn.close()
|
|
return [_row_to_thought(r) for r in rows]
|
|
|
|
def get_thought(self, thought_id: str) -> Thought | None:
|
|
"""Retrieve a single thought by ID."""
|
|
conn = _get_conn(self._db_path)
|
|
row = conn.execute("SELECT * FROM thoughts WHERE id = ?", (thought_id,)).fetchone()
|
|
conn.close()
|
|
return _row_to_thought(row) if row else None
|
|
|
|
def get_thought_chain(self, thought_id: str, max_depth: int = 20) -> list[Thought]:
|
|
"""Follow the parent chain backward from a thought.
|
|
|
|
Returns thoughts in chronological order (oldest first).
|
|
"""
|
|
chain = []
|
|
current_id: str | None = thought_id
|
|
conn = _get_conn(self._db_path)
|
|
|
|
for _ in range(max_depth):
|
|
if not current_id:
|
|
break
|
|
row = conn.execute("SELECT * FROM thoughts WHERE id = ?", (current_id,)).fetchone()
|
|
if not row:
|
|
break
|
|
chain.append(_row_to_thought(row))
|
|
current_id = row["parent_id"]
|
|
|
|
conn.close()
|
|
chain.reverse() # Chronological order
|
|
return chain
|
|
|
|
def count_thoughts(self) -> int:
|
|
"""Return total number of stored thoughts."""
|
|
conn = _get_conn(self._db_path)
|
|
count = conn.execute("SELECT COUNT(*) as c FROM thoughts").fetchone()["c"]
|
|
conn.close()
|
|
return count
|
|
|
|
# ── Private helpers ──────────────────────────────────────────────────
|
|
|
|
def _gather_seed(self) -> tuple[str, str]:
|
|
"""Pick a seed type and gather relevant context.
|
|
|
|
Returns (seed_type, seed_context_string).
|
|
"""
|
|
seed_type = random.choice(SEED_TYPES)
|
|
|
|
if seed_type == "swarm":
|
|
return seed_type, self._seed_from_swarm()
|
|
if seed_type == "scripture":
|
|
return seed_type, self._seed_from_scripture()
|
|
if seed_type == "memory":
|
|
return seed_type, self._seed_from_memory()
|
|
if seed_type == "creative":
|
|
prompt = random.choice(_CREATIVE_SEEDS)
|
|
return seed_type, f"Creative prompt: {prompt}"
|
|
if seed_type == "existential":
|
|
prompt = random.choice(_EXISTENTIAL_SEEDS)
|
|
return seed_type, f"Reflection: {prompt}"
|
|
if seed_type == "sovereignty":
|
|
prompt = random.choice(_SOVEREIGNTY_SEEDS)
|
|
return seed_type, f"Sovereignty reflection: {prompt}"
|
|
if seed_type == "observation":
|
|
return seed_type, self._seed_from_observation()
|
|
# freeform — no seed, pure continuation
|
|
return seed_type, ""
|
|
|
|
# Reflective prompts layered on top of swarm data
|
|
_SWARM_REFLECTIONS = [
|
|
"What does this activity pattern tell me about the health of the system?",
|
|
"Which tasks are flowing smoothly, and where is friction building up?",
|
|
"If I were coaching these agents, what would I suggest they focus on?",
|
|
"Is the swarm balanced, or is one agent carrying too much weight?",
|
|
"What surprised me about recent task outcomes?",
|
|
]
|
|
|
|
def _seed_from_swarm(self) -> str:
|
|
"""Gather recent swarm activity as thought seed with a reflective prompt."""
|
|
try:
|
|
from datetime import timedelta
|
|
|
|
from timmy.briefing import _gather_swarm_summary, _gather_task_queue_summary
|
|
|
|
since = datetime.now(UTC) - timedelta(hours=1)
|
|
swarm = _gather_swarm_summary(since)
|
|
tasks = _gather_task_queue_summary()
|
|
reflection = random.choice(self._SWARM_REFLECTIONS)
|
|
return (
|
|
f"Recent swarm activity: {swarm}\n"
|
|
f"Task queue: {tasks}\n\n"
|
|
f"Reflect on this: {reflection}"
|
|
)
|
|
except Exception as exc:
|
|
logger.debug("Swarm seed unavailable: %s", exc)
|
|
return "The swarm is quiet right now. What does silence in a system mean?"
|
|
|
|
def _seed_from_scripture(self) -> str:
|
|
"""Gather current scripture meditation focus as thought seed."""
|
|
return "Scripture is on my mind, though no specific verse is in focus."
|
|
|
|
def _seed_from_memory(self) -> str:
|
|
"""Gather memory context as thought seed."""
|
|
try:
|
|
from timmy.memory_system import memory_system
|
|
|
|
context = memory_system.get_system_context()
|
|
if context:
|
|
# Truncate to a reasonable size for a thought seed
|
|
return f"From my memory:\n{context[:500]}"
|
|
except Exception as exc:
|
|
logger.debug("Memory seed unavailable: %s", exc)
|
|
return "My memory vault is quiet."
|
|
|
|
def _seed_from_observation(self) -> str:
|
|
"""Ground a thought in concrete recent activity and a reflective prompt."""
|
|
prompt = random.choice(_OBSERVATION_SEEDS)
|
|
# Pull real data to give the model something concrete to reflect on
|
|
context_parts = [f"Observation prompt: {prompt}"]
|
|
try:
|
|
from datetime import timedelta
|
|
|
|
from timmy.briefing import _gather_swarm_summary, _gather_task_queue_summary
|
|
|
|
since = datetime.now(UTC) - timedelta(hours=2)
|
|
swarm = _gather_swarm_summary(since)
|
|
tasks = _gather_task_queue_summary()
|
|
if swarm:
|
|
context_parts.append(f"Recent activity: {swarm}")
|
|
if tasks:
|
|
context_parts.append(f"Queue: {tasks}")
|
|
except Exception as exc:
|
|
logger.debug("Observation seed data unavailable: %s", exc)
|
|
return "\n".join(context_parts)
|
|
|
|
def _build_continuity_context(self) -> str:
|
|
"""Build context from the last few thoughts for chain continuity."""
|
|
recent = self.get_recent_thoughts(limit=3)
|
|
if not recent:
|
|
return "This is your first thought since waking up."
|
|
|
|
lines = ["Your recent thoughts:"]
|
|
# recent is newest-first, reverse for chronological order
|
|
for thought in reversed(recent):
|
|
lines.append(f"- [{thought.seed_type}] {thought.content}")
|
|
return "\n".join(lines)
|
|
|
|
def _call_agent(self, prompt: str) -> str:
|
|
"""Call Timmy's agent to generate a thought.
|
|
|
|
Uses a separate session_id to avoid polluting user chat history.
|
|
"""
|
|
try:
|
|
from timmy.session import chat
|
|
|
|
return chat(prompt, session_id="thinking")
|
|
except Exception:
|
|
# Fallback: create a fresh agent
|
|
from timmy.agent import create_timmy
|
|
|
|
agent = create_timmy()
|
|
run = agent.run(prompt, stream=False)
|
|
return run.content if hasattr(run, "content") else str(run)
|
|
|
|
def _store_thought(self, content: str, seed_type: str) -> Thought:
|
|
"""Persist a thought to SQLite."""
|
|
thought = Thought(
|
|
id=str(uuid.uuid4()),
|
|
content=content,
|
|
seed_type=seed_type,
|
|
parent_id=self._last_thought_id,
|
|
created_at=datetime.now(UTC).isoformat(),
|
|
)
|
|
|
|
conn = _get_conn(self._db_path)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO thoughts (id, content, seed_type, parent_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
""",
|
|
(thought.id, thought.content, thought.seed_type, thought.parent_id, thought.created_at),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return thought
|
|
|
|
def _log_event(self, thought: Thought) -> None:
|
|
"""Log the thought as a swarm event."""
|
|
try:
|
|
from swarm.event_log import EventType, log_event
|
|
|
|
log_event(
|
|
EventType.TIMMY_THOUGHT,
|
|
source="thinking-engine",
|
|
agent_id="default",
|
|
data={
|
|
"thought_id": thought.id,
|
|
"seed_type": thought.seed_type,
|
|
"content": thought.content[:200],
|
|
},
|
|
)
|
|
except Exception as exc:
|
|
logger.debug("Failed to log thought event: %s", exc)
|
|
|
|
def _write_journal(self, thought: Thought) -> None:
|
|
"""Append the thought to a daily markdown journal file.
|
|
|
|
Writes to data/journal/YYYY-MM-DD.md — one file per day, append-only.
|
|
"""
|
|
try:
|
|
ts = datetime.fromisoformat(thought.created_at)
|
|
journal_dir = self._db_path.parent / "journal"
|
|
journal_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
journal_file = journal_dir / f"{ts.strftime('%Y-%m-%d')}.md"
|
|
time_str = ts.strftime("%I:%M %p").lstrip("0")
|
|
|
|
entry = f"## {time_str} — {thought.seed_type}\n\n{thought.content}\n\n---\n\n"
|
|
|
|
with open(journal_file, "a", encoding="utf-8") as f:
|
|
f.write(entry)
|
|
except Exception as exc:
|
|
logger.debug("Failed to write journal entry: %s", exc)
|
|
|
|
async def _broadcast(self, thought: Thought) -> None:
|
|
"""Broadcast the thought to WebSocket clients."""
|
|
try:
|
|
from infrastructure.ws_manager.handler import ws_manager
|
|
|
|
await ws_manager.broadcast(
|
|
"timmy_thought",
|
|
{
|
|
"thought_id": thought.id,
|
|
"content": thought.content,
|
|
"seed_type": thought.seed_type,
|
|
"created_at": thought.created_at,
|
|
},
|
|
)
|
|
except Exception as exc:
|
|
logger.debug("Failed to broadcast thought: %s", exc)
|
|
|
|
|
|
# Module-level singleton
|
|
thinking_engine = ThinkingEngine()
|