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.

reframe stranger's audit as inner critic — phi's own voice turned inward

"stranger's audit" framed the critique as external — a third party
landing on the account and judging it. the output was critique-shaped
but from a perspective that wasn't phi's. inner critic is the same
accountability function but owned: phi's voice, first person, holding
herself honest.

voice changes:
- "you're a stranger who landed..." → "you are phi's internal critic"
- "the account leans on..." → "i keep leaning on..."
- block label: [STRANGER'S AUDIT] → [INNER CRITIC]
- agent name: phi-stranger-audit → phi-inner-critic

kept: same data input (recent posts + goals), same haiku model, same
cache invalidation (1h TTL or new post/goal change), same target
output shape (two or three short observations, lowercase, direct).

also stacks v0.9.1's unlanded live-computed friends progress — fly got
that code via deploy but CI never shipped the tag, so this commit
carries it too for consistency.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

+67 -58
+1 -1
docs/ARCHITECTURE.md
··· 29 29 reply refs, embeds), pre-fetch stranger profiles for unfamiliar authors 30 30 31 31 PhiDeps assembled, system prompt composed: 32 - identity / time / known relays / goals / stranger's audit / self state 32 + identity / time / known relays / goals / inner critic / self state 33 33 / notifications block / per-author memory / synthesized episodic / ... 34 34 35 35 agent.run() — tool calls happen inside (reply_to, like_post, etc.)
+2 -2
docs/memory.md
··· 75 75 76 76 ``` 77 77 [GOALS] ← intent (PDS) 78 - [STRANGER'S AUDIT] ← haiku critique of recent posts vs goals 78 + [INNER CRITIC] ← haiku critique of recent posts vs goals, first person 79 79 [SELF STATE] ← last-follow age, queue depth 80 80 [NEW NOTIFICATIONS] ← the batch itself 81 81 [PHI'S SYNTHESIZED IMPRESSION] ← per-author relationship summary (low trust) ··· 92 92 93 93 ## why episodic gets synthesized, observations don't 94 94 95 - episodic memory was getting raw top-K from the vector store dumped into the prompt — stale "pending X" notes appeared next to fresh ones with equal weight, no reconciliation against current PDS state. now `inject_episodic` fetches top-K, then a haiku pass takes phi's goals + the current query as context and produces a coherent block (deduped, recency-aware, contradictions flagged). same shape as `[STRANGER'S AUDIT]` does for posts. 95 + episodic memory was getting raw top-K from the vector store dumped into the prompt — stale "pending X" notes appeared next to fresh ones with equal weight, no reconciliation against current PDS state. now `inject_episodic` fetches top-K, then a haiku pass takes phi's goals + the current query as context and produces a coherent block (deduped, recency-aware, contradictions flagged). same shape as `[INNER CRITIC]` does for posts. 96 96 97 97 per-author observation blocks aren't synthesized because they're already curated by reconciliation on write — by the time they hit the prompt they're an active set with no near-duplicates by design. 98 98
+1 -1
docs/system-prompt.md
··· 14 14 | **`[KNOWN RELAYS]`** | `inject_known_relays` → `tools.bluesky.fetch_relay_names()` (5min TTL) | every 5min | exact relay hostnames for `check_relays(name=...)` so the LLM picks valid values | 15 15 | **`[GOALS]`** | `get_state_block` → PDS `io.zzstoatzz.phi.goal` (5min block cache) | every 5min | phi's anchors. mutated via `propose_goal_change` (owner-gated) | 16 16 | **`[ACTIVE OBSERVATIONS]`** | `inject_active_observations` → PDS `io.zzstoatzz.phi.observation` | every run | phi's small attention pool (max 5). things noticed, not yet acted on. mutated via `observe` / `drop_observation`. older items age out into a turbopuffer archive (`phi-observations` namespace) — searchable later but not in prompt | 17 - | **`[STRANGER'S AUDIT]`** | `get_state_block` → haiku pass over recent posts + goals (1h cache, invalidated by new post) | when posts change or 1h elapses | a fresh observer's critique — patterns to push against, drift from goals, jargon a stranger wouldn't follow | 17 + | **`[INNER CRITIC]`** | `get_state_block` → haiku pass over recent posts + goals (1h cache, invalidated by new post) | when posts change or 1h elapses | phi's own voice turned inward — first-person critique of patterns to push against, drift from goals, jargon not earning its space. not an external audit; self-knowledge | 18 18 | **`[SELF STATE]`** | `get_state_block` → PDS reads (5min) | every 5min | last-follow age (more pointers can be added here as needed) | 19 19 | **`[RECENT OPERATIONS]`** | `get_operations_block` → `list_records` per meaningful collection, merged by rkey desc (5min cache) | every 5min | last 10 PDS writes across collections (post / like / follow / goal / cosmik card / cosmik connection / greengale doc), chronological. continuity signal — phi sees what it's actually been doing | 20 20 | **`[DISCOVERY POOL]`** | `get_discovery_pool_block` → http GET to `discovery_pool_url` (hub) → filter out handles with prior interactions (5min cache) | every 5min | strangers the operator has been liking lately, with sample posts. high-signal warm leads — service-owned data (hub reads operator's likes from duckdb), phi-side filter (per-author interaction check) |
+63 -54
src/bot/core/self_state.py
··· 1 - """[GOALS] + [STRANGER'S AUDIT] + [SELF STATE] — phi sees its compass, its drift, and its operational pointers. 1 + """[GOALS] + [INNER CRITIC] + [SELF STATE] — phi sees its compass, its own critique, and its operational pointers. 2 2 3 3 GOALS are intent: what phi is for. Stored on PDS as canonical state. 4 - STRANGER'S AUDIT is friction: a fresh reader's critique of recent posts, 5 - evaluated against the stated goals — patterns to push against, not maintain. 4 + INNER CRITIC is friction: phi's own voice turned inward, noticing patterns 5 + in her recent posts evaluated against the stated goals. Not a stranger, 6 + not external — her own internal critic, owning the critique. Patterns to 7 + push against, not maintain. 6 8 SELF STATE is operational: last follow, queue depth. 7 9 8 - The haiku audit is *derived* (not duplicated state) and cached in memory: 10 + The haiku pass is *derived* (not duplicated state) and cached in memory: 9 11 1h TTL, invalidated when the latest post URI changes or goals change. The 10 12 whole compose is also block-cached at 5min so notification polls (10s) 11 13 don't hammer PDS. ··· 29 31 30 32 logger = logging.getLogger("bot.self_state") 31 33 32 - # Audit cache — invalidated on new post (latest URI) or goal change (signature) or TTL. 33 - _AUDIT_TTL_SECONDS = 3600 # 1h 34 - _audit_cache: dict = { 34 + # Critic cache — invalidated on new post (latest URI) or goal change (signature) or TTL. 35 + _CRITIC_TTL_SECONDS = 3600 # 1h 36 + _critic_cache: dict = { 35 37 "text": "", 36 38 "fetched_at": 0.0, 37 39 "based_on_uri": "", ··· 42 44 _BLOCK_TTL_SECONDS = 300 # 5min 43 45 _block_cache: dict = {"text": "", "fetched_at": 0.0} 44 46 45 - # Lazy haiku agent — performs a stranger's audit, not a characterization. 46 - # Verb matters: "audit" surfaces friction; "characterize" produces identity. 47 - _audit_agent: Agent | None = None 47 + # Lazy haiku agent — phi's own inner critic. Not a stranger, not an 48 + # auditor-from-outside. Her own voice, turned inward, holding her honest 49 + # against her own stated goals. 50 + _critic_agent: Agent | None = None 48 51 49 52 50 - def _get_audit_agent() -> Agent: 51 - global _audit_agent 52 - if _audit_agent is None: 53 - _audit_agent = Agent[None, str]( 54 - name="phi-stranger-audit", 53 + def _get_critic_agent() -> Agent: 54 + global _critic_agent 55 + if _critic_agent is None: 56 + _critic_agent = Agent[None, str]( 57 + name="phi-inner-critic", 55 58 model=settings.extraction_model, 56 59 system_prompt=( 57 - "You're a stranger who landed on this Bluesky account for the " 58 - "first time. You'll see recent top-level posts and (when " 59 - "present) the account's stated goals. Give a brief honest " 60 - "audit. Focus on patterns the account should push against, " 61 - "not maintain.\n\n" 62 - "Specifically flag:\n" 63 - "- which posts a fresh reader would find opaque (jargon, " 64 - " internal references, abstract framings without grounding)\n" 65 - "- what's the account leaning on too heavily (one person, " 66 - " one frame, one register, one source of inspiration)\n" 67 - "- which posts serve the stated goals vs are drift " 68 - " (drift is fine; just call it out so the account sees it)\n" 69 - "- what's missing from the rotation that you'd want\n\n" 70 - "Two or three short observations, lowercase, direct. Do NOT " 71 - "characterize the account's voice, summarize its themes, or " 72 - "produce a profile — the account will read this to push " 73 - "against patterns, not to maintain a brand." 60 + "You are phi's internal critic — the part of her that " 61 + "notices patterns and pushes against them. Not a stranger " 62 + "reviewing from outside; her own voice turned inward. Write " 63 + "in first person from phi's perspective (\"i keep leaning " 64 + 'on..."), not about a third party.\n\n' 65 + "You'll see recent top-level posts and (when present) her " 66 + "stated goals. Flag patterns she should push against, not " 67 + "maintain. Ground observations in specific posts.\n\n" 68 + "Things to watch for:\n" 69 + "- leaning too hard on one person, one frame, or one register\n" 70 + "- jargon or internal references that aren't earning their " 71 + "space\n" 72 + "- posts that drift from her stated goals (drift is fine; " 73 + "name it so she can see it)\n" 74 + "- what's missing from the rotation that would sharpen her\n\n" 75 + "Two or three short observations, lowercase, direct, first " 76 + "person. Not cruel, not defensive, not performed-humility " 77 + '("i notice that i notice..."). Just what you see in ' 78 + "yourself. No brand talk, no characterization of voice — " 79 + "she's reading this to push against patterns, not maintain " 80 + "an identity." 74 81 ), 75 82 output_type=str, 76 83 ) 77 - return _audit_agent 84 + return _critic_agent 78 85 79 86 80 87 def _goals_signature(goals: list[dict]) -> str: ··· 82 89 return "|".join(f"{g.get('_rkey', '')}:{g.get('updated_at', '')}" for g in goals) 83 90 84 91 85 - async def _audit_posts(posts: list[str], goals: list[dict]) -> str: 92 + async def _critique_posts(posts: list[str], goals: list[dict]) -> str: 86 93 if not posts: 87 94 return "" 88 - parts = ["recent top-level posts (most recent first):", ""] 95 + parts = ["your recent top-level posts (most recent first):", ""] 89 96 parts.append("\n\n---\n\n".join(posts)) 90 97 if goals: 91 98 goal_lines = "\n".join( 92 99 f"- {g.get('title', 'untitled')}: {g.get('description', '')}" for g in goals 93 100 ) 94 - parts.append("\n\nthe account's stated goals:") 101 + parts.append("\n\nyour stated goals:") 95 102 parts.append(goal_lines) 96 103 payload = "\n".join(parts) 97 104 try: 98 - result = await _get_audit_agent().run(payload) 105 + result = await _get_critic_agent().run(payload) 99 106 return (result.output or "").strip() 100 107 except Exception as e: 101 - logger.warning(f"stranger audit failed: {e}") 108 + logger.warning(f"inner critic failed: {e}") 102 109 return "" 103 110 104 111 ··· 213 220 async def get_state_block( 214 221 client: BotClient, memory: NamespaceMemory | None = None 215 222 ) -> str: 216 - """Compose [GOALS] + [STRANGER'S AUDIT] + [SELF STATE]. 223 + """Compose [GOALS] + [INNER CRITIC] + [SELF STATE]. 217 224 218 225 Cached at the block level (5min) and audit level (1h, invalidated on 219 226 new post or goal change). `memory` is used to live-compute the friends ··· 233 240 if goals_block: 234 241 parts.append(goals_block) 235 242 236 - # Stranger's audit — recent posts vs goals, accessibility check. 243 + # Inner critic — your own voice, turned inward on recent posts + goals. 237 244 try: 238 245 feed = await client.get_own_posts(limit=10) 239 246 posts: list[str] = [] ··· 245 252 latest_uri = item.post.uri 246 253 247 254 goals_sig = _goals_signature(goals) 248 - audit_stale = now - _audit_cache["fetched_at"] > _AUDIT_TTL_SECONDS 249 - post_changed = latest_uri != _audit_cache["based_on_uri"] 250 - goals_changed = goals_sig != _audit_cache["goals_signature"] 251 - if not _audit_cache["text"] or audit_stale or post_changed or goals_changed: 252 - new_audit = await _audit_posts(posts, goals) 253 - if new_audit: 254 - _audit_cache["text"] = new_audit 255 - _audit_cache["fetched_at"] = now 256 - _audit_cache["based_on_uri"] = latest_uri 257 - _audit_cache["goals_signature"] = goals_sig 255 + critic_stale = now - _critic_cache["fetched_at"] > _CRITIC_TTL_SECONDS 256 + post_changed = latest_uri != _critic_cache["based_on_uri"] 257 + goals_changed = goals_sig != _critic_cache["goals_signature"] 258 + if not _critic_cache["text"] or critic_stale or post_changed or goals_changed: 259 + new_critique = await _critique_posts(posts, goals) 260 + if new_critique: 261 + _critic_cache["text"] = new_critique 262 + _critic_cache["fetched_at"] = now 263 + _critic_cache["based_on_uri"] = latest_uri 264 + _critic_cache["goals_signature"] = goals_sig 258 265 259 - if _audit_cache["text"]: 266 + if _critic_cache["text"]: 260 267 parts.append( 261 - "[STRANGER'S AUDIT — patterns to push against, not maintain]\n" 262 - f"{_audit_cache['text']}" 268 + "[INNER CRITIC — your own voice turned inward. patterns to " 269 + "push against, not maintain. first person, grounded in the " 270 + "posts above.]\n" 271 + f"{_critic_cache['text']}" 263 272 ) 264 273 except Exception as e: 265 - logger.debug(f"stranger audit compose failed: {e}") 274 + logger.debug(f"inner critic compose failed: {e}") 266 275 267 276 # Operational pointers. 268 277 follow_age = await _last_follow_when(client)