diff --git a/WORKSET_PLAN_PHASE2.md b/WORKSET_PLAN_PHASE2.md
new file mode 100644
index 00000000..2c9355ed
--- /dev/null
+++ b/WORKSET_PLAN_PHASE2.md
@@ -0,0 +1,133 @@
+# Timmy Time โ Workset Plan Phase 2 (Functional Hardening)
+
+**Date:** 2026-02-25
+**Based on:** QUALITY_ANALYSIS.md remaining issues
+
+---
+
+## Executive Summary
+
+This workset addresses the core functional gaps that prevent the swarm system from operating as designed. The swarm currently registers agents in the database but doesn't actually spawn processes or execute bids. This workset makes the swarm operational.
+
+---
+
+## Workset E: Swarm System Realization ๐
+
+### E1: Real Agent Process Spawning (FUNC-01)
+**Priority:** P1 โ High
+**Files:** `swarm/agent_runner.py`, `swarm/coordinator.py`
+
+**Issue:** `spawn_agent()` creates a database record but no Python process is actually launched.
+
+**Fix:**
+- Complete the `agent_runner.py` subprocess implementation
+- Ensure spawned agents can communicate with coordinator
+- Add proper lifecycle management (start, monitor, stop)
+
+### E2: Working Auction System (FUNC-02)
+**Priority:** P1 โ High
+**Files:** `swarm/bidder.py`, `swarm/persona_node.py`
+
+**Issue:** Bidding system runs auctions but no actual agents submit bids.
+
+**Fix:**
+- Connect persona agents to the bidding system
+- Implement automatic bid generation based on capabilities
+- Ensure auction resolution assigns tasks to winners
+
+### E3: Persona Agent Auto-Bidding
+**Priority:** P1 โ High
+**Files:** `swarm/persona_node.py`, `swarm/coordinator.py`
+
+**Fix:**
+- Spawned persona agents should automatically bid on matching tasks
+- Implement capability-based bid decisions
+- Add bid amount calculation (base + jitter)
+
+---
+
+## Workset F: Testing & Reliability ๐งช
+
+### F1: WebSocket Reconnection Tests (TEST-01)
+**Priority:** P2 โ Medium
+**Files:** `tests/test_websocket.py`
+
+**Issue:** WebSocket tests don't cover reconnection logic or malformed payloads.
+
+**Fix:**
+- Add reconnection scenario tests
+- Test malformed payload handling
+- Test connection failure recovery
+
+### F2: Voice TTS Graceful Degradation
+**Priority:** P2 โ Medium
+**Files:** `timmy_serve/voice_tts.py`, `dashboard/routes/voice.py`
+
+**Issue:** Voice routes fail without clear message when `pyttsx3` not installed.
+
+**Fix:**
+- Add graceful fallback message
+- Return helpful error suggesting `pip install ".[voice]"`
+- Don't crash, return 503 with instructions
+
+### F3: Mobile Route Navigation
+**Priority:** P2 โ Medium
+**Files:** `templates/base.html`
+
+**Issue:** `/mobile` route not linked from desktop navigation.
+
+**Fix:**
+- Add mobile link to base template nav
+- Make it easy to find mobile-optimized view
+
+---
+
+## Workset G: Performance & Architecture โก
+
+### G1: SQLite Connection Pooling (PERF-01)
+**Priority:** P3 โ Low
+**Files:** `swarm/registry.py`
+
+**Issue:** New SQLite connection opened on every query.
+
+**Fix:**
+- Implement connection pooling or singleton pattern
+- Reduce connection overhead
+- Maintain thread safety
+
+### G2: Development Experience
+**Priority:** P2 โ Medium
+**Files:** `Makefile`, `README.md`
+
+**Issue:** No single command to start full dev environment.
+
+**Fix:**
+- Add `make dev-full` that starts dashboard + Ollama check
+- Add better startup validation
+
+---
+
+## Execution Order
+
+| Order | Workset | Task | Est. Time |
+|-------|---------|------|-----------|
+| 1 | E | Persona auto-bidding system | 45 min |
+| 2 | E | Fix auction resolution | 30 min |
+| 3 | F | Voice graceful degradation | 20 min |
+| 4 | F | Mobile nav link | 10 min |
+| 5 | G | SQLite connection pooling | 30 min |
+| 6 | โ | Test everything | 30 min |
+
+**Total: ~2.5 hours**
+
+---
+
+## Success Criteria
+
+- [ ] Persona agents automatically bid on matching tasks
+- [ ] Auctions resolve with actual winners
+- [ ] Voice routes degrade gracefully without pyttsx3
+- [ ] Mobile route accessible from desktop nav
+- [ ] SQLite connections pooled/reused
+- [ ] All 895+ tests pass
+- [ ] New tests for bidding system
diff --git a/src/dashboard/routes/swarm.py b/src/dashboard/routes/swarm.py
index b10a0d7e..0a3453d1 100644
--- a/src/dashboard/routes/swarm.py
+++ b/src/dashboard/routes/swarm.py
@@ -4,6 +4,7 @@ Provides REST endpoints for managing the swarm: listing agents,
spawning sub-agents, posting tasks, and viewing auction results.
"""
+import asyncio
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
@@ -90,8 +91,10 @@ async def list_tasks(status: Optional[str] = None):
@router.post("/tasks")
async def post_task(description: str = Form(...)):
- """Post a new task to the swarm for bidding."""
+ """Post a new task to the swarm and run auction to assign it."""
task = coordinator.post_task(description)
+ # Start auction asynchronously - don't wait for it to complete
+ asyncio.create_task(coordinator.run_auction_and_assign(task.id))
return {
"task_id": task.id,
"description": task.description,
diff --git a/src/dashboard/templates/base.html b/src/dashboard/templates/base.html
index d3449063..112112fa 100644
--- a/src/dashboard/templates/base.html
+++ b/src/dashboard/templates/base.html
@@ -30,6 +30,7 @@
MARKET
TOOLS
CREATIVE
+ MOBILE
diff --git a/src/swarm/coordinator.py b/src/swarm/coordinator.py
index 538e17d1..940e7b1e 100644
--- a/src/swarm/coordinator.py
+++ b/src/swarm/coordinator.py
@@ -367,7 +367,7 @@ class SwarmCoordinator:
async def _broadcast_agent_joined(self, agent_id: str, name: str) -> None:
"""Broadcast agent joined event via WebSocket."""
try:
- from websocket.handler import ws_manager
+ from ws_manager.handler import ws_manager
await ws_manager.broadcast_agent_joined(agent_id, name)
except Exception as exc:
logger.debug("WebSocket broadcast failed (agent_joined): %s", exc)
@@ -375,7 +375,7 @@ class SwarmCoordinator:
async def _broadcast_bid(self, task_id: str, agent_id: str, bid_sats: int) -> None:
"""Broadcast bid submitted event via WebSocket."""
try:
- from websocket.handler import ws_manager
+ from ws_manager.handler import ws_manager
await ws_manager.broadcast_bid_submitted(task_id, agent_id, bid_sats)
except Exception as exc:
logger.debug("WebSocket broadcast failed (bid): %s", exc)
@@ -383,7 +383,7 @@ class SwarmCoordinator:
async def _broadcast_task_posted(self, task_id: str, description: str) -> None:
"""Broadcast task posted event via WebSocket."""
try:
- from websocket.handler import ws_manager
+ from ws_manager.handler import ws_manager
await ws_manager.broadcast_task_posted(task_id, description)
except Exception as exc:
logger.debug("WebSocket broadcast failed (task_posted): %s", exc)
@@ -391,7 +391,7 @@ class SwarmCoordinator:
async def _broadcast_task_assigned(self, task_id: str, agent_id: str) -> None:
"""Broadcast task assigned event via WebSocket."""
try:
- from websocket.handler import ws_manager
+ from ws_manager.handler import ws_manager
await ws_manager.broadcast_task_assigned(task_id, agent_id)
except Exception as exc:
logger.debug("WebSocket broadcast failed (task_assigned): %s", exc)
@@ -401,7 +401,7 @@ class SwarmCoordinator:
) -> None:
"""Broadcast task completed event via WebSocket."""
try:
- from websocket.handler import ws_manager
+ from ws_manager.handler import ws_manager
await ws_manager.broadcast_task_completed(task_id, agent_id, result)
except Exception as exc:
logger.debug("WebSocket broadcast failed (task_completed): %s", exc)
diff --git a/src/swarm/registry.py b/src/swarm/registry.py
index 4f0671db..79107944 100644
--- a/src/swarm/registry.py
+++ b/src/swarm/registry.py
@@ -15,21 +15,8 @@ from typing import Optional
DB_PATH = Path("data/swarm.db")
-@dataclass
-class AgentRecord:
- id: str = field(default_factory=lambda: str(uuid.uuid4()))
- name: str = ""
- status: str = "idle" # idle | busy | offline
- capabilities: str = "" # comma-separated tags
- registered_at: str = field(
- default_factory=lambda: datetime.now(timezone.utc).isoformat()
- )
- last_seen: str = field(
- default_factory=lambda: datetime.now(timezone.utc).isoformat()
- )
-
-
def _get_conn() -> sqlite3.Connection:
+ """Get a SQLite connection."""
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
@@ -49,6 +36,20 @@ def _get_conn() -> sqlite3.Connection:
return conn
+@dataclass
+class AgentRecord:
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
+ name: str = ""
+ status: str = "idle" # idle | busy | offline
+ capabilities: str = "" # comma-separated tags
+ registered_at: str = field(
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
+ )
+ last_seen: str = field(
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
+ )
+
+
def _row_to_record(row: sqlite3.Row) -> AgentRecord:
return AgentRecord(
id=row["id"],
@@ -67,70 +68,81 @@ def register(name: str, capabilities: str = "", agent_id: Optional[str] = None)
capabilities=capabilities,
)
conn = _get_conn()
- conn.execute(
- """
- INSERT OR REPLACE INTO agents (id, name, status, capabilities, registered_at, last_seen)
- VALUES (?, ?, ?, ?, ?, ?)
- """,
- (record.id, record.name, record.status, record.capabilities,
- record.registered_at, record.last_seen),
- )
- conn.commit()
- conn.close()
+ try:
+ conn.execute(
+ """
+ INSERT OR REPLACE INTO agents (id, name, status, capabilities, registered_at, last_seen)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (record.id, record.name, record.status, record.capabilities,
+ record.registered_at, record.last_seen),
+ )
+ conn.commit()
+ finally:
+ conn.close()
return record
def unregister(agent_id: str) -> bool:
conn = _get_conn()
- cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,))
- conn.commit()
- deleted = cursor.rowcount > 0
- conn.close()
- return deleted
+ try:
+ cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,))
+ conn.commit()
+ return cursor.rowcount > 0
+ finally:
+ conn.close()
def get_agent(agent_id: str) -> Optional[AgentRecord]:
conn = _get_conn()
- row = conn.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone()
- conn.close()
- return _row_to_record(row) if row else None
+ try:
+ row = conn.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone()
+ return _row_to_record(row) if row else None
+ finally:
+ conn.close()
def list_agents(status: Optional[str] = None) -> list[AgentRecord]:
conn = _get_conn()
- if status:
- rows = conn.execute(
- "SELECT * FROM agents WHERE status = ? ORDER BY registered_at DESC",
- (status,),
- ).fetchall()
- else:
- rows = conn.execute(
- "SELECT * FROM agents ORDER BY registered_at DESC"
- ).fetchall()
- conn.close()
- return [_row_to_record(r) for r in rows]
+ try:
+ if status:
+ rows = conn.execute(
+ "SELECT * FROM agents WHERE status = ? ORDER BY registered_at DESC",
+ (status,),
+ ).fetchall()
+ else:
+ rows = conn.execute(
+ "SELECT * FROM agents ORDER BY registered_at DESC"
+ ).fetchall()
+ return [_row_to_record(r) for r in rows]
+ finally:
+ conn.close()
def update_status(agent_id: str, status: str) -> Optional[AgentRecord]:
now = datetime.now(timezone.utc).isoformat()
conn = _get_conn()
- conn.execute(
- "UPDATE agents SET status = ?, last_seen = ? WHERE id = ?",
- (status, now, agent_id),
- )
- conn.commit()
- conn.close()
- return get_agent(agent_id)
+ try:
+ conn.execute(
+ "UPDATE agents SET status = ?, last_seen = ? WHERE id = ?",
+ (status, now, agent_id),
+ )
+ conn.commit()
+ return get_agent(agent_id)
+ finally:
+ conn.close()
def heartbeat(agent_id: str) -> Optional[AgentRecord]:
"""Update last_seen timestamp for a registered agent."""
now = datetime.now(timezone.utc).isoformat()
conn = _get_conn()
- conn.execute(
- "UPDATE agents SET last_seen = ? WHERE id = ?",
- (now, agent_id),
- )
- conn.commit()
- conn.close()
- return get_agent(agent_id)
+ try:
+ conn.execute(
+ "UPDATE agents SET last_seen = ? WHERE id = ?",
+ (now, agent_id),
+ )
+ conn.commit()
+ return get_agent(agent_id)
+ finally:
+ conn.close()