a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: remove unused approval system, document for future reference

removed 164 lines of orphaned code from database.py that was never
integrated with the current MCP-based architecture. the approval system
was designed for conditional self-modification (phi proposes changes,
operator approves), but was left behind during the pydanticai refactor.

documented the original design and future integration options in
sandbox/APPROVAL_SYSTEM.md for when we want to reintroduce this capability.

this change makes phi fully stateless - all dynamic state now lives in
remote services (turbopuffer for memories, atproto for threads). the only
local files are ephemeral caches (.session) and configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

zzstoatzz 6c5db0b8 013dfffd

+174 -164
+174
sandbox/APPROVAL_SYSTEM.md
··· 1 + # approval system (deprecated) 2 + 3 + ## purpose 4 + 5 + the approval system was designed to enable phi to modify itself through conditional operator permission. the idea: phi could take certain actions that would be executed only after the operator (nate) explicitly approved them. 6 + 7 + ## use case: self-modification 8 + 9 + the primary motivation was **personality/identity editing through empirical learning**. for example: 10 + 11 + 1. phi observes through interactions that certain responses work better 12 + 2. phi proposes a modification to its personality file or core memories 13 + 3. this proposal is stored as an "approval request" in sqlite 14 + 4. the operator is notified (via bluesky thread or other channel) 15 + 5. operator reviews and approves/denies via some interface 16 + 6. if approved, phi applies the change to itself 17 + 18 + ## implementation (removed) 19 + 20 + the system was implemented in `src/bot/database.py` (now removed) with: 21 + 22 + ### database schema 23 + ```sql 24 + CREATE TABLE approval_requests ( 25 + id INTEGER PRIMARY KEY AUTOINCREMENT, 26 + request_type TEXT NOT NULL, -- e.g., "personality_edit", "memory_update" 27 + request_data TEXT NOT NULL, -- JSON with the proposed change 28 + status TEXT NOT NULL DEFAULT 'pending', -- 'pending', 'approved', 'denied', 'expired' 29 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 30 + resolved_at TIMESTAMP, 31 + resolver_comment TEXT, 32 + applied_at TIMESTAMP, 33 + thread_uri TEXT, -- bluesky thread where request was made 34 + notified_at TIMESTAMP, -- when thread was notified of resolution 35 + operator_notified_at TIMESTAMP -- when operator was notified of request 36 + ) 37 + ``` 38 + 39 + ### api methods 40 + - `create_approval_request(request_type, request_data, thread_uri)` - create new request 41 + - `get_pending_approvals(include_notified=True)` - fetch pending requests 42 + - `resolve_approval(approval_id, approved, comment)` - approve/deny 43 + - `get_approval_by_id(approval_id)` - fetch specific request 44 + - `mark_approval_notified(approval_id)` - mark thread notified 45 + - `mark_operator_notified(approval_ids)` - mark operator notified 46 + 47 + ## why it was removed 48 + 49 + the approval system was never integrated with the current MCP-based architecture. it was built for an earlier iteration of phi and became orphaned code (164 lines) during the refactor to pydanticai + MCP. 50 + 51 + ## future integration considerations 52 + 53 + if we want to reintroduce self-modification with approval, here's how it could work with the current architecture: 54 + 55 + ### option 1: mcp tool for approval requests 56 + 57 + create an MCP tool `request_operator_approval(action_type, proposal)` that: 58 + 1. stores the request in turbopuffer (not sqlite) with metadata 59 + 2. posts to a dedicated bluesky thread for operator review 60 + 3. operator replies with "approved" or "denied" 61 + 4. phi polls for operator's response and executes if approved 62 + 63 + **pros:** 64 + - uses existing memory infrastructure (turbopuffer) 65 + - natural interface (bluesky threads) 66 + - no additional database needed 67 + 68 + **cons:** 69 + - approval state is in turbopuffer, which is append-only 70 + - need to poll bluesky threads for operator responses 71 + 72 + ### option 2: dedicated approval service 73 + 74 + build a separate service (fastapi endpoint or slack bot) that: 75 + 1. phi calls via MCP tool 76 + 2. service sends notification to operator (email, slack, webhook) 77 + 3. operator approves via web UI or slack command 78 + 4. service stores approval in postgres/sqlite 79 + 5. phi polls service for approval status 80 + 81 + **pros:** 82 + - clean separation of concerns 83 + - flexible notification channels 84 + - persistent approval history 85 + 86 + **cons:** 87 + - more infrastructure 88 + - another service to run and maintain 89 + 90 + ### option 3: human-in-the-loop via pydanticai 91 + 92 + use pydanticai's built-in human-in-the-loop features: 93 + 1. agent proposes action that requires approval 94 + 2. pydanticai pauses execution and waits for human input 95 + 3. operator provides approval via some interface 96 + 4. agent resumes and executes 97 + 98 + **pros:** 99 + - leverages pydanticai primitives 100 + - minimal custom code 101 + 102 + **cons:** 103 + - unclear how this works with async/notification-driven architecture 104 + - may require blocking operations 105 + 106 + ## recommended approach 107 + 108 + if we reintroduce this, i'd recommend **option 1** (mcp tool + turbopuffer): 109 + 110 + ```python 111 + # in MCP server 112 + @server.tool() 113 + async def request_operator_approval( 114 + action_type: str, # "personality_edit", "memory_update", etc. 115 + proposal: str, # description of what phi wants to do 116 + justification: str # why phi thinks this is a good idea 117 + ) -> str: 118 + """request operator approval for a self-modification action""" 119 + 120 + # store in turbopuffer with special namespace 121 + approval_id = await memory.store_approval_request( 122 + action_type=action_type, 123 + proposal=proposal, 124 + justification=justification 125 + ) 126 + 127 + # post to operator's bluesky mentions 128 + await atproto.post( 129 + f"🤖 approval request #{approval_id}\n\n" 130 + f"action: {action_type}\n" 131 + f"proposal: {proposal}\n\n" 132 + f"justification: {justification}\n\n" 133 + f"reply 'approve' or 'deny'" 134 + ) 135 + 136 + return f"approval request #{approval_id} submitted" 137 + ``` 138 + 139 + then in the notification handler, check for operator replies to approval threads and execute the approved action. 140 + 141 + ## examples of self-modification actions 142 + 143 + what kinds of things might phi want operator approval for? 144 + 145 + 1. **personality edits** - "i notice people respond better when i'm more concise. can i add 'prefer brevity' to my guidelines?" 146 + 147 + 2. **capability expansion** - "i've been asked about weather 5 times this week. can i add a weather API tool?" 148 + 149 + 3. **memory pruning** - "i have 10,000 memories for @alice but most are low-value small talk. can i archive memories older than 30 days with low importance?" 150 + 151 + 4. **behavior changes** - "i'm getting rate limited on likes. can i reduce my like threshold from 0.7 to 0.8?" 152 + 153 + 5. **relationship updates** - "based on our conversations, i think @bob prefers technical depth over casual chat. can i update his user context?" 154 + 155 + ## philosophical notes 156 + 157 + self-modification with approval is interesting because: 158 + 159 + - it preserves operator agency (you control what phi becomes) 160 + - it enables empirical learning (phi adapts based on real interactions) 161 + - it creates a collaborative evolution (phi proposes, you decide) 162 + 163 + but it also raises questions: 164 + 165 + - what if phi proposes changes you don't understand? 166 + - what if approval becomes a bottleneck (too many requests)? 167 + - what if phi learns to game the approval system? 168 + 169 + worth thinking through before reintroducing. 170 + 171 + ## references 172 + 173 + - original implementation: `git log --all --grep="approval"` (if committed) 174 + - related: `sandbox/void_self_modification.md` (void's approach to self-modification)
sandbox/threads.db.archive

This is a binary file and will not be displayed.

-164
src/bot/database.py
··· 1 - """Simple SQLite database for approval requests""" 2 - 3 - import sqlite3 4 - from contextlib import contextmanager 5 - from pathlib import Path 6 - from typing import Any 7 - 8 - 9 - class ThreadDatabase: 10 - """Database for storing approval requests (future self-modification features)""" 11 - 12 - def __init__(self, db_path: Path = Path("threads.db")): 13 - self.db_path = db_path 14 - self._init_db() 15 - 16 - def _init_db(self): 17 - """Initialize database schema""" 18 - with self._get_connection() as conn: 19 - # Approval requests table 20 - conn.execute(""" 21 - CREATE TABLE IF NOT EXISTS approval_requests ( 22 - id INTEGER PRIMARY KEY AUTOINCREMENT, 23 - request_type TEXT NOT NULL, 24 - request_data TEXT NOT NULL, 25 - status TEXT NOT NULL DEFAULT 'pending', 26 - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 27 - resolved_at TIMESTAMP, 28 - resolver_comment TEXT, 29 - applied_at TIMESTAMP, 30 - thread_uri TEXT, 31 - notified_at TIMESTAMP, 32 - operator_notified_at TIMESTAMP, 33 - CHECK (status IN ('pending', 'approved', 'denied', 'expired')) 34 - ) 35 - """) 36 - conn.execute(""" 37 - CREATE INDEX IF NOT EXISTS idx_approval_status 38 - ON approval_requests(status) 39 - """) 40 - 41 - # Add missing columns if they don't exist (migrations) 42 - for column in ["notified_at", "operator_notified_at"]: 43 - try: 44 - conn.execute(f"ALTER TABLE approval_requests ADD COLUMN {column} TIMESTAMP") 45 - except sqlite3.OperationalError: 46 - # Column already exists 47 - pass 48 - 49 - @contextmanager 50 - def _get_connection(self): 51 - """Get database connection""" 52 - conn = sqlite3.connect(self.db_path) 53 - conn.row_factory = sqlite3.Row 54 - try: 55 - yield conn 56 - conn.commit() 57 - finally: 58 - conn.close() 59 - 60 - def create_approval_request( 61 - self, request_type: str, request_data: str, thread_uri: str | None = None 62 - ) -> int: 63 - """Create a new approval request and return its ID""" 64 - import json 65 - 66 - with self._get_connection() as conn: 67 - cursor = conn.execute( 68 - """ 69 - INSERT INTO approval_requests (request_type, request_data, thread_uri) 70 - VALUES (?, ?, ?) 71 - """, 72 - (request_type, json.dumps(request_data) if isinstance(request_data, dict) else request_data, thread_uri), 73 - ) 74 - return cursor.lastrowid 75 - 76 - def get_pending_approvals(self, include_notified: bool = True) -> list[dict[str, Any]]: 77 - """Get pending approval requests 78 - 79 - Args: 80 - include_notified: If False, only return approvals not yet notified to operator 81 - """ 82 - with self._get_connection() as conn: 83 - if include_notified: 84 - cursor = conn.execute( 85 - """ 86 - SELECT * FROM approval_requests 87 - WHERE status = 'pending' 88 - ORDER BY created_at ASC 89 - """ 90 - ) 91 - else: 92 - cursor = conn.execute( 93 - """ 94 - SELECT * FROM approval_requests 95 - WHERE status = 'pending' AND operator_notified_at IS NULL 96 - ORDER BY created_at ASC 97 - """ 98 - ) 99 - return [dict(row) for row in cursor.fetchall()] 100 - 101 - def resolve_approval( 102 - self, approval_id: int, approved: bool, comment: str = "" 103 - ) -> bool: 104 - """Resolve an approval request""" 105 - with self._get_connection() as conn: 106 - cursor = conn.execute( 107 - """ 108 - UPDATE approval_requests 109 - SET status = ?, resolved_at = CURRENT_TIMESTAMP, resolver_comment = ? 110 - WHERE id = ? AND status = 'pending' 111 - """, 112 - ("approved" if approved else "denied", comment, approval_id), 113 - ) 114 - return cursor.rowcount > 0 115 - 116 - def get_approval_by_id(self, approval_id: int) -> dict[str, Any] | None: 117 - """Get a specific approval request by ID""" 118 - with self._get_connection() as conn: 119 - cursor = conn.execute( 120 - "SELECT * FROM approval_requests WHERE id = ?", 121 - (approval_id,), 122 - ) 123 - row = cursor.fetchone() 124 - return dict(row) if row else None 125 - 126 - def get_recently_applied_approvals(self) -> list[dict[str, Any]]: 127 - """Get approvals that were recently applied and need thread notification""" 128 - with self._get_connection() as conn: 129 - cursor = conn.execute( 130 - """ 131 - SELECT * FROM approval_requests 132 - WHERE status = 'approved' 133 - AND applied_at IS NOT NULL 134 - AND thread_uri IS NOT NULL 135 - AND (notified_at IS NULL OR notified_at < applied_at) 136 - ORDER BY applied_at DESC 137 - """ 138 - ) 139 - return [dict(row) for row in cursor.fetchall()] 140 - 141 - def mark_approval_notified(self, approval_id: int) -> bool: 142 - """Mark that we've notified the thread about this approval""" 143 - with self._get_connection() as conn: 144 - cursor = conn.execute( 145 - "UPDATE approval_requests SET notified_at = CURRENT_TIMESTAMP WHERE id = ?", 146 - (approval_id,), 147 - ) 148 - return cursor.rowcount > 0 149 - 150 - def mark_operator_notified(self, approval_ids: list[int]) -> int: 151 - """Mark that we've notified the operator about these approvals""" 152 - if not approval_ids: 153 - return 0 154 - with self._get_connection() as conn: 155 - placeholders = ",".join("?" * len(approval_ids)) 156 - cursor = conn.execute( 157 - f"UPDATE approval_requests SET operator_notified_at = CURRENT_TIMESTAMP WHERE id IN ({placeholders})", 158 - approval_ids, 159 - ) 160 - return cursor.rowcount 161 - 162 - 163 - # Global database instance 164 - thread_db = ThreadDatabase()