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.

fix reviewer findings: full unmute reversibility, evidence dedup, fail on mute error

- unmute endpoint now supersedes the turbopuffer spam marker (not just
the platform mute), so phi treats the account as a stranger again
- remove duplicate evidence append — store_exploration_note already
adds evidence_uris
- fail the queue item if mute() throws instead of completing with a
half-done state

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

zzstoatzz b975389c 6d9f417b

+60 -10
+1 -1
loq.toml
··· 21 21 22 22 [[rules]] 23 23 path = "src/bot/main.py" 24 - max_lines = 846 24 + max_lines = 851
+3 -6
src/bot/agent.py
··· 687 687 bot_client.client.mute(resolved.did) 688 688 except Exception as e: 689 689 logger.warning(f"failed to mute {subject}: {e}") 690 + await fail(rkey) 691 + return 0 690 692 # store one user-scoped marker so is_stranger() sees it 691 693 if self.memory: 692 694 reason = output.mute_reason or output.summary[:150] 693 - evidence = ( 694 - f" [evidence: {', '.join(output.mute_evidence)}]" 695 - if output.mute_evidence 696 - else "" 697 - ) 698 695 await self.memory.store_exploration_note( 699 696 handle=subject, 700 - content=f"muted — {reason}{evidence}", 697 + content=f"muted — {reason}", 701 698 tags=["muted", "spam"], 702 699 evidence_uris=output.mute_evidence, 703 700 )
+8 -3
src/bot/main.py
··· 417 417 418 418 @app.post("/api/control/unmute") 419 419 async def unmute_account(request: Request): 420 - """Unmute an account by handle.""" 420 + """Unmute an account by handle — reverses both the platform mute and the memory marker.""" 421 421 if err := _check_control_token(request): 422 422 return err 423 423 body = await request.json() ··· 428 428 await bot_client.authenticate() 429 429 resolved = bot_client.client.resolve_handle(handle) 430 430 bot_client.client.unmute(resolved.did) 431 - logger.info(f"unmuted @{handle}") 432 - return {"unmuted": handle} 431 + # also clear the private spam marker so phi treats them as a stranger again 432 + poller: NotificationPoller | None = getattr(app.state, "poller", None) 433 + marker_cleared = False 434 + if poller and poller.handler.agent.memory: 435 + marker_cleared = await poller.handler.agent.memory.clear_mute_marker(handle) 436 + logger.info(f"unmuted @{handle} (marker_cleared={marker_cleared})") 437 + return {"unmuted": handle, "marker_cleared": marker_cleared} 433 438 except Exception as e: 434 439 logger.error(f"failed to unmute @{handle}: {e}") 435 440 return JSONResponse({"error": str(e)}, status_code=500)
+48
src/bot/memory/namespace_memory.py
··· 775 775 ) 776 776 logger.info(f"stored exploration note for @{handle}: {content[:80]}") 777 777 778 + async def clear_mute_marker(self, handle: str) -> bool: 779 + """Supersede any muted/spam exploration notes for a handle. 780 + 781 + Returns True if a marker was found and superseded. 782 + """ 783 + user_ns = self.get_user_namespace(handle) 784 + try: 785 + response = user_ns.query( 786 + rank_by=("created_at", "desc"), 787 + top_k=5, 788 + filters=[ 789 + "And", 790 + [ 791 + ["kind", "Eq", "exploration_note"], 792 + ["status", "Eq", "active"], 793 + ["tags", "ContainsAll", ["muted"]], 794 + ], 795 + ], 796 + include_attributes=["content", "tags"], 797 + ) 798 + if not response.rows: 799 + return False 800 + now = datetime.now().isoformat() 801 + for row in response.rows: 802 + user_ns.write( 803 + upsert_rows=[ 804 + { 805 + "id": row.id, 806 + "vector": row.vector, 807 + "kind": "exploration_note", 808 + "status": "superseded", 809 + "content": row.content, 810 + "tags": getattr(row, "tags", []), 811 + "supersedes": "", 812 + "created_at": getattr(row, "created_at", now), 813 + "updated_at": now, 814 + } 815 + ], 816 + distance_metric="cosine_distance", 817 + schema=USER_NAMESPACE_SCHEMA, 818 + ) 819 + logger.info(f"cleared mute marker for @{handle}") 820 + return True 821 + except Exception as e: 822 + if "was not found" in str(e): 823 + return False 824 + raise 825 + 778 826 async def get_knowledge_count(self, handle: str) -> int: 779 827 """Count observations + exploration notes phi has stored about a handle. 780 828