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.

discovery: route through bot, rename feed→activity, reframe copy

three changes per feedback:

1. /api/discovery on the bot — single source of truth
the frontend was calling hub.waow.tech/api/agents/discovery-pool
directly while bot/core/discovery_pool.py applied phi's per-author
interaction filter before injecting the prompt block. so the public
page showed a different list than what phi was reasoning over —
conceptual drift waiting to bite.

fix: extract get_filtered_pool() in core/discovery_pool.py (called
by both the prompt block and the new /api/discovery endpoint) so
the public view = phi's view by construction. frontend now calls
/api/discovery (relative); HUB_URL constant removed from web/api.ts.

2. /feed → /activity
the page is phi's public *output* (bsky posts + cosmik notes/cards),
not a feed. renamed the route folder, the nav label, and the home
page link. content unchanged. copy clarifies "what she's emitted
into the world."

3. /discovery copy reframed
header now leads with "what surfaces for attention" rather than
describing the data source first. notes that operator-likes is one
signal among possible others (future sources can feed the same
surface) without prescribing what those would be.

deliberately NOT done (pushback on reviewer's broader IA proposal):
- no /radar route — phi doesn't currently use saved feeds as a
discovery source, so a radar page would surface config that isn't
load-bearing. premature.
- kept plain "activity" / "discovery" labels rather than the more
performative "public cognition" / "attention pipeline."

102 python tests pass, frontend bun run check + build clean.

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

+85 -22
+20 -10
src/bot/core/discovery_pool.py
··· 108 108 return "\n".join(lines) 109 109 110 110 111 - async def get_discovery_pool_block(memory: NamespaceMemory | None) -> str: 112 - """Fetch + filter + render the [DISCOVERY POOL] block. Cached 5min.""" 113 - now = time.time() 114 - if _block_cache["text"] and now - _block_cache["fetched_at"] < _BLOCK_TTL_SECONDS: 115 - return _block_cache["text"] 116 - 111 + async def get_filtered_pool( 112 + memory: NamespaceMemory | None, top_n: int = TOP_N 113 + ) -> list[_Entry]: 114 + """Fetch the operator-likes pool, drop self + handles phi has already 115 + interacted with, return the top-N. This is the canonical "what phi 116 + actually sees in her prompt" view; the JSON API endpoint and the 117 + rendered prompt block both compose from this single source of truth. 118 + """ 117 119 raw = await _fetch_pool() 118 120 if not raw: 119 - _block_cache["text"] = "" 120 - _block_cache["fetched_at"] = now 121 - return "" 121 + return [] 122 122 123 123 if memory is not None: 124 124 kept: list[_Entry] = [] ··· 131 131 kept.append(entry) 132 132 raw = kept 133 133 134 - block = _render(raw[:TOP_N]) 134 + return raw[:top_n] 135 + 136 + 137 + async def get_discovery_pool_block(memory: NamespaceMemory | None) -> str: 138 + """Fetch + filter + render the [DISCOVERY POOL] block. Cached 5min.""" 139 + now = time.time() 140 + if _block_cache["text"] and now - _block_cache["fetched_at"] < _BLOCK_TTL_SECONDS: 141 + return _block_cache["text"] 142 + 143 + entries = await get_filtered_pool(memory) 144 + block = _render(entries) 135 145 _block_cache["text"] = block 136 146 _block_cache["fetched_at"] = now 137 147 return block
+38
src/bot/main.py
··· 25 25 26 26 from bot.config import settings 27 27 from bot.core.atproto_client import bot_client 28 + from bot.core.discovery_pool import get_filtered_pool 28 29 from bot.core.profile_manager import ProfileManager 29 30 from bot.logging_config import _clear_uvicorn_handlers 30 31 from bot.memory import NamespaceMemory ··· 180 181 background_tasks.add_task(poller.handler.review_memories) 181 182 logger.info("memory review triggered via API") 182 183 return {"triggered": True} 184 + 185 + 186 + _discovery_cache_data: list | None = None 187 + _discovery_cache_expires: float = 0.0 188 + _DISCOVERY_CACHE_TTL = 60 # seconds 189 + 190 + 191 + @app.get("/api/discovery") 192 + async def discovery(): 193 + """Discovery pool — filtered to what phi actually sees in her prompt. 194 + 195 + Joins the upstream operator-likes pool (hub) with phi's per-author 196 + interaction state. The frontend reads this so the public page stays 197 + aligned with phi's view; previously it called hub directly and showed 198 + a different (raw) list than the one phi was reasoning over. 199 + """ 200 + global _discovery_cache_data, _discovery_cache_expires 201 + now = time.monotonic() 202 + if _discovery_cache_data is not None and now < _discovery_cache_expires: 203 + return JSONResponse(_discovery_cache_data) 204 + 205 + memory: NamespaceMemory | None = None 206 + if settings.turbopuffer_api_key and settings.openai_api_key: 207 + try: 208 + memory = NamespaceMemory(api_key=settings.turbopuffer_api_key) 209 + except Exception as e: 210 + logger.debug(f"discovery: memory client init failed: {e}") 211 + 212 + try: 213 + entries = await get_filtered_pool(memory) 214 + except Exception as e: 215 + logger.warning(f"discovery: get_filtered_pool failed: {e}") 216 + return JSONResponse([], status_code=200) 217 + 218 + _discovery_cache_data = entries 219 + _discovery_cache_expires = now + _DISCOVERY_CACHE_TTL 220 + return JSONResponse(entries) 183 221 184 222 185 223 _graph_cache_data: dict | None = None
+8 -5
web/src/lib/api.ts
··· 17 17 export const PHI_DID = 'did:plc:65sucjiel52gefhcdcypynsr'; 18 18 export const PHI_HANDLE = 'phi.zzstoatzz.io'; 19 19 export const OWNER_HANDLE = 'zzstoatzz.io'; 20 - export const HUB_URL = 'https://hub.waow.tech'; 21 20 22 21 const BSKY_PUBLIC = 'https://public.api.bsky.app'; 23 22 const PDS_HOST = 'https://bsky.social'; ··· 139 138 return data.feed; 140 139 } 141 140 142 - // --- hub discovery pool --- 141 + // --- discovery pool --- 142 + // 143 + // frontend calls the bot's /api/discovery (NOT hub directly), so the public 144 + // page reflects the same filtered list phi sees in her prompt — operator 145 + // likes minus handles phi has already exchanged with. single source of 146 + // truth lives in bot/core/discovery_pool.py:get_filtered_pool. 143 147 144 - export async function getDiscoveryPool(maxAuthors = 30): Promise<DiscoveryEntry[]> { 145 - const url = `${HUB_URL}/api/agents/discovery-pool?max_authors=${maxAuthors}`; 148 + export async function getDiscoveryPool(): Promise<DiscoveryEntry[]> { 146 149 try { 147 - const res = await fetch(url); 150 + const res = await fetch('/api/discovery'); 148 151 if (!res.ok) return []; 149 152 return await res.json(); 150 153 } catch {
+1 -1
web/src/lib/components/Nav.svelte
··· 3 3 4 4 const links = [ 5 5 { href: '/', label: 'phi' }, 6 - { href: '/feed', label: 'feed' }, 6 + { href: '/activity', label: 'activity' }, 7 7 { href: '/mind', label: 'mind' }, 8 8 { href: '/blog', label: 'blog' }, 9 9 { href: '/discovery', label: 'discovery' },
+1 -1
web/src/routes/+page.svelte
··· 74 74 <section> 75 75 <div class="section-header"> 76 76 <h2>recent activity</h2> 77 - <a href="/feed" class="more">see all →</a> 77 + <a href="/activity" class="more">see all →</a> 78 78 </div> 79 79 {#if !loaded} 80 80 <p class="faint">loading…</p>
+12 -3
web/src/routes/discovery/+page.svelte
··· 17 17 <header> 18 18 <h1>discovery</h1> 19 19 <p class="muted"> 20 - authors @{OWNER_HANDLE} has been liking lately. high-signal pool of attention. phi sees a 21 - filtered version of this in her own context (with people she's already exchanged with removed). 20 + what surfaces for attention. high-signal candidates phi sees in her prompt — strangers worth 21 + considering. matches what phi sees: the upstream pool minus people she's already engaged with. 22 + </p> 23 + <p class="source faint"> 24 + source: @{OWNER_HANDLE}'s recent likes (one signal among possible others; future sources can 25 + feed the same surface). 22 26 </p> 23 27 </header> 24 28 25 29 {#if !loaded} 26 30 <p class="faint">loading…</p> 27 31 {:else if entries.length === 0} 28 - <p class="faint">no recent activity to show.</p> 32 + <p class="faint">nothing surfacing right now.</p> 29 33 {:else} 30 34 {#each entries as entry (entry.did)} 31 35 <DiscoveryCard {entry} /> ··· 42 46 max-width: 600px; 43 47 font-size: 13px; 44 48 line-height: 1.5; 49 + } 50 + 51 + .source { 52 + font-size: 12px; 53 + margin-top: 8px; 45 54 } 46 55 </style>
+5 -2
web/src/routes/feed/+page.svelte web/src/routes/activity/+page.svelte
··· 30 30 31 31 <div class="container"> 32 32 <header> 33 - <h1>feed</h1> 34 - <p class="muted">phi's recent posts, notes, and bookmarked URLs across surfaces.</p> 33 + <h1>activity</h1> 34 + <p class="muted"> 35 + phi's public output — bluesky posts, semble notes, and bookmarked URLs. what 36 + she's emitted into the world across surfaces. 37 + </p> 35 38 </header> 36 39 37 40 <div class="filters">