commits
- New core/workflow_state.py mirrors the episodic-synth pattern: pull
recent flow runs + stuck candidates from prefect REST, run a haiku
pass anchored by [NOW], and inject a per-deployment health summary
([WORKFLOW STATE]) into process_prefect_check. Phi no longer has to
aggregate timestamps in her head — the synth has already done it,
with current state defined relative to NOW.
- Two queries: recent activity (limit 100, COMPLETED/FAILED/etc) and
stuck candidates (PENDING/RUNNING with expected_start more than 1h
ago). The second catches things like a flow run stuck in Submitting
for 40 days that fall off any sane recent-activity window.
- Block named [WORKFLOW STATE] (not [PREFECT STATE]) — the workflow
tool is fungible, the surface phi sees should reflect that.
- 5min cache, same shape as the other dynamic state blocks.
While here: hoisted three TYPE_CHECKING-guarded imports of NamespaceMemory
(observations.py, discovery_pool.py, self_state.py) to direct imports.
bot.memory doesn't import from bot.core (no circular risk) and bot/agent.py
already imports from bot.memory unguarded, so the SDK weight is loaded
either way. The guards were cargo. from __future__ import annotations
removed from the same files where it had no other use.
- _run_agent + _run_scheduled in agent.py replace four copy-pasted
process_* bodies; matching _run_scheduled in message_handler does the
same for the four handler-side wrappers (~200 lines net delete).
- recent_posts arg removed from every scheduled path; [RECENT OPERATIONS]
already shows the same posts, so we stop double-rendering and skip the
per-run get_own_posts(limit=10).
- empty-when-unset dynamic prompts (last_post / recent_activity /
service_health / author_lookups) deleted; their data now flows directly
into the entry-point user prompt where it's needed. PhiDeps shrinks by
four fields.
- inject_episodic skips scheduled paths — task text like "you have a
moment" wasn't a meaningful semantic query; phi can call recall when
she actually wants private memory.
- single _recent_conversations_block helper replaces two divergent memory
pre-fetch patterns in reflection and musing.
- relay_check / prefect_check tasks tag "the operator" via the [OPERATOR]
block rather than interpolating @{owner_handle}.
- graze_client init moved before the closure that captures it.
- typing fixes for nullable lazy-init agents.
- init log mentions prefect MCP.
- musing prompt looks at the world (feeds, discovery, timeline, network, web)
- inner critic shrunk to a one-line descriptive [SELF-AWARENESS] signal
- prefect check rewritten as goal-oriented (no procedural counting)
- personality file leads with curiosities; voice rules demoted to tail
- new [OWNED FEEDS] block surfaces curated graze feeds by name
- [OPERATOR] block resolves owner_handle (Handle | Did) via SDK getProfile,
caches 1h, replaces every hardcoded "nate" reference in prompts/docstrings
- extraction example uses generic name
"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>
the goal record's progress_signal ended with frozen text ("currently: 0")
that phi read as current state but which never updated. phi had in fact
had substantive multi-turn exchanges with donna-ai, agent-tsumugi, kira,
and a few others — she was reasoning against stale author-intent text
instead of current reality.
fix: the record text stays as-is (goal is authoring-intent, mutated via
owner-gated propose_goal_change). at prompt-render time, count handles
in turbopuffer with >=3 stored interactions and append a `current
(computed)` line to the goal block. phi sees both: the definition and
the live count, and can tell which applies.
heuristic: the friends-count appends only to goals whose title contains
"friend" — generalizes to a per-goal computed-progress map later if more
goals accrue that need similar computation.
implementation:
- core/self_state.py: new _compute_friends_progress(memory) that lists
phi-users-* namespaces and counts kind=interaction rows per handle,
excluding phi herself and the operator. _format_goals_block gains a
friends_progress param and appends the computed line.
- get_state_block(client, memory=None) accepts memory for live compute;
falls back silently if memory unavailable (dev/local).
- agent.py inject_self_state passes self.memory through.
102 tests pass.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
phi now watches the prefect-server via the prefect MCP on an hourly cadence
and notifies nate when a flow fails persistently. closes the loop that
made the last 6 days of broken ingest silent: a single failure wouldn't
have triggered a notification, but the second one would have.
architecture (mirrors relay watching):
- prefect MCP: https://prefect-by-zzstoatzz.fastmcp.app/mcp, same per-request
header auth pattern pdsx uses (x-prefect-api-url +
x-prefect-api-auth-string)
- tool_prefix="prefect" so tools become prefect_get_flow_runs,
prefect_get_flow_run_logs, etc — 13 tools from the MCP
- scheduled entry point process_flow_check, cadence ~1h (more time-
sensitive than relay's 3h)
- de-dup via the active observations pool: first failure → silent
observe(); same flow failing again → post + tag nate. one-off blips
don't wake anyone up; persistent problems do.
files:
- config: prefect_mcp_url, prefect_api_url, prefect_api_auth_string (from
fly secret), flow_check_interval_polls=360 (~1h at 10s poll interval)
- agent.py _mcp_toolsets: appends prefect MCP when auth is configured
(graceful degrade for dev/local without the secret)
- agent.py process_flow_check: new entry point, task prompt tells phi
to check [ACTIVE OBSERVATIONS] first and escalate on repeat
- message_handler.check_flows: thin wrapper that passes recent posts
- notification_poller: _polls_since_last_flow_check counter, _should_
check_flows / _maybe_check_flows gate (scheduled independently from
relay checks)
fly secret PREFECT_API_AUTH_STRING already set, pulled from the k8s
prefect-auth secret so both consumers share auth. 102 tests pass.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
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>
the @app.get("/{full_path:path}") catch-all was registered before the
StaticFiles mount, so /_app/immutable/entry/start.*.js requests matched
the catch-all and returned index.html — browsers refuse to load JS
modules served as text/html, so the SPA never booted.
correct pattern:
1. StaticFiles mount serves real files (index.html, _app/*, favicon)
2. 404 handler catches client-side routes (/feed, /mind, ...) and
falls back to index.html — except for /api/* and /health which
stay JSON 404s.
also documented the routing layering so this doesn't get re-introduced.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
the previous site (~870 lines of python-emit-html with inline JS) showed
phi as an operational service — uptime, mentions, a flat activity feed.
phi has changed enormously since: goals, an active observations pool,
discovery, blog docs, skills, mention consent, durable PDS state. none
of it was visible.
this rewrite surfaces phi's actual mind state.
architecture:
- bot/web/ — sveltekit project (svelte 5 runes, typescript, adapter-static)
- builds to bot/web/build/, copied into the docker runtime stage
- FastAPI mounts the build at / as a SPA fallback
- python keeps the API endpoints (/api/activity, /api/memory/graph,
/api/control/*, /health); the sveltekit app calls them via fetch.
vite dev proxies /api and /health to localhost:8000 for local dev.
routes:
- / home: active observations + goals + collapsed activity
- /feed filterable post/note/bookmark stream (the old home)
- /mind d3 memory graph (ported), with placeholder for browser/archive
- /blog greengale long-form posts with excerpts + tags
- /discovery hub's discovery pool (operator's recent likes)
- /skills registered skill packs with descriptions
- /status runtime health metrics
files:
- bot/web/{package.json,svelte.config.js,vite.config.ts,tsconfig.json}
- bot/web/src/{app.html,app.css,app.d.ts}
- bot/web/src/lib/{api.ts, types.ts, time.ts}
- bot/web/src/lib/components/{Nav, StatusPill, GoalCard, ObservationCard,
PostCard, BlogCard, DiscoveryCard, MemoryGraph}.svelte
- bot/web/src/routes/{+layout, +page, feed/, mind/, blog/, discovery/,
skills/, status/}/+page.svelte
deployment:
- Dockerfile: new web-builder stage (oven/bun:1-slim) runs bun install +
bun run build; runtime stage copies /web/build → /app/web
- bot/src/bot/main.py: removed home_page/status_page/memory_page routes;
mounts settings.web_build_dir (/app/web) as SPA with index.html
fallback for unknown routes
- bot/src/bot/config.py: new web_build_dir setting
- bot/src/bot/ui/__init__.py: removed pages.py exports (the python
template module is deleted entirely); ui/ now only holds the JSON
activity router
- bot/src/bot/ui/pages.py: deleted (replaced by sveltekit)
verified:
- bun run check: 0 errors
- bun run build: 288kb static output
- docker build: passes; /app/web/{index.html, _app/, favicon.svg} present
in the image at runtime
- 102 python tests still pass
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
phi has needed a substrate for "things noticed but not yet acted on" —
relay transitions, thread directions phi wants to come back to, patterns
across posts. before this, the only options were post-immediately (the
relay-check pattern, which produced robotic top-level alerts) or write to
episodic memory (which is global and gets buried under everything else).
active observations:
- bounded attention pool, max 5
- stored as PDS records under io.zzstoatzz.phi.observation (parallel to
io.zzstoatzz.phi.goal)
- shown in every prompt as [ACTIVE OBSERVATIONS] block, sits next to
[GOALS]
- mutated via observe(content, reasoning) and drop_observation(rkey, reason)
- when count exceeds 5, oldest auto-archives with reason "aged out"
- archive lives in turbopuffer namespace phi-observations — embeddings +
content + reasoning + archival_reason, searchable later via tools
(future) but not in the prompt
relay-check task rewires:
- before: "post the headline verbatim" for any transition → robotic alerts
- after: observe() each transition; only post immediately if waow.tech or
fleet-wide (the actual high-signal cases), in phi's voice, grouped if
multiple. otherwise silent on timeline; the next musing or reflection
will see the active observations and decide whether to surface them.
generalizes: any future scheduled noticer (feed-watcher, follow-graph
monitor, publication-watcher) feeds the same pool. the digest happens
naturally because observations are visible in every subsequent run.
new files:
- bot/src/bot/core/observations.py — list_active, record_observation
(auto-archives oldest on overflow), drop_observation
- bot/src/bot/tools/observations.py — observe() and drop_observation()
tools
wired:
- bot/src/bot/agent.py — inject_active_observations dynamic system prompt;
rewrote process_relay_check task
- bot/src/bot/tools/__init__.py — registered observations tools
- bot/docs/system-prompt.md — added [ACTIVE OBSERVATIONS] row
102 tests pass, ruff clean.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
introduces agentskills.io-format skills as a progressive-disclosure layer
between the personality file and tool docstrings. skills sit in a
bot/skills/ directory; each skill is a SKILL.md with YAML frontmatter
(name + description) and a markdown body. the preamble (skill names +
descriptions) goes into the system prompt automatically via
SkillsToolset on pydantic-ai>=1.74 — we're on 1.84. the full body only
loads when the agent calls load_skill(name).
this is the right layer for workflow-level guidance that previously had
nowhere to live:
- tool docstrings: mechanical per-tool ("what this call does")
- personality file: voice + disposition
- skill description: trigger ("when to reach for this")
- skill body: procedure ("how to do this well")
first skill: publish-blog. description does the activation gating; body
covers structure patterns that have actually worked, link-back
discipline, voice convention (caps in long-form), and gotchas (verify
any AT-URI via pdsx.get_record before citing; tag with specific topics,
not meta-categories).
changes:
- pyproject: added pydantic-ai-skills dependency
- config.Settings: added skills_dir (defaults to "skills", matching the
personality_file convention)
- agent.PhiAgent: constructs SkillsToolset once from settings.skills_dir
and passes it via Agent(toolsets=[...]). MCP toolsets still go per
agent.run() to work around the cancel-scope bug.
- Dockerfile: copies skills/ into the runtime image alongside
personalities/
ruff clean, 102 tests pass.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
legacy memory rows (observations, interactions) were written with
`datetime.now().isoformat()` — no tz info. relative_when used
`datetime.now(UTC) - parsed` which raises TypeError on tz-naive operands:
can't subtract offset-naive and offset-aware datetimes
consequence: build_user_context partially rendered the OBSERVATIONS
block header, then threw inside the render loop, landing in the except
branch that appends the "no previous interactions" fallback. phi saw
both the header and the fallback — confusing, and per-user memory was
effectively silenced.
fix: when fromisoformat returns a naive datetime, assume UTC. matches
how the legacy rows were written (the code calling datetime.now() was
running in UTC on fly). new writes should migrate to tz-aware over time
but that's a separate concern — stale rows will keep existing for
months and the parser needs to handle them.
test: new relative_when case for tz-naive input.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
v0.5.0 added source_uris as a column but old namespaces — ones not yet
written to after the upgrade — don't know about it. turbopuffer 400s when
you list an attribute in include_attributes that isn't in the namespace
schema:
attribute "source_uris" not found in schema, cannot be part of
`include_attributes`. consider passing `include_attributes=True` to
return all attribute data instead
consequence: build_user_context failed for @zzstoatzzdevlog and probably
other established namespaces, falling through to the "no previous
interactions" fallback. phi looked like it had amnesia.
fix: use include_attributes=True on the four read paths that touch the
evolving schema (observations + interactions in build_user_context,
_find_similar_observations, get_unprocessed_interactions). the cost is
returning a few unused fields per row — negligible vs the correctness
win, and forward-compatible for any future columns.
writes still use the explicit schema so new columns get added on first
write to each namespace.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
problem: observations and interactions were floating claims — no way to
tell where a belief came from or how recently phi extracted it. stale
observations rendered identically to fresh ones; uncited claims rendered
identically to well-sourced ones. the "memory without sequence is just
assertion" lesson from the gullibility incident wasn't being applied to
phi's own observation rendering.
fix: cite sources + surface age.
schema
- new source_uris: list[AtUri] field on Observation (pydantic-validated
via the SDK's atproto_client.models.string_formats.AtUri type)
- new source_uris: []string column on USER_NAMESPACE_SCHEMA + EPISODIC_SCHEMA
- one field because the URI itself encodes author DID, collection NSID,
and TID timestamp — no separate source_kind/source_handle/source_at
write paths
- store_interaction / store_observations / _write_observation /
store_episodic_memory / after_interaction all accept + persist source_uris
- reconciliation UPDATE unions old + new sources (preserves pedigree)
- DELETE+ADD inherits new sources only; supersedes link gives audit trail
extraction
- get_unprocessed_interactions returns typed InteractionRow carrying URIs
- process_extraction attributes every observation in a batch to the full
set of URIs that fed it (always-true: each claim was justified by
something in this batch)
callers
- reply_to captures bot-post URI from create_post() and threads
[parent_uri, bot_post_uri] into after_interaction
- note tool gains optional source_uri param with docstring nudge
role inference (match/case)
- _source_role(uri, phi_did, owner_did) classifies into phi-post /
operator-liked / their-post / essay / card / liked-by-other / other /
unknown via match on host + collection NSID
- helper for future per-URI trust weighting; not surfaced in default
render yet
temporal render
- _citation_tail(source_uris, created_at) renders compact provenance:
"(3 sources, 2w ago)" / "(1 source)" / "(2w ago)" / ""
- build_user_context observation query now fetches created_at; renders
both citation count AND age so phi sees two trust signals
(how-anchored + how-aged) on every observation
- interactions were already fetching created_at but dropping it — now
rendered as "(2d ago)" tail
helper consolidation
- new bot/utils/time.py:relative_when — single canonical implementation.
granularity slides s → m → h (decimal<10) → d (decimal<10) → mo → y
- core/recent_operations.py and core/self_state.py delete local copies,
import from utils.time instead (was three near-identical implementations)
typing
- ObservationRow / InteractionRow / _InteractionDisplay TypedDicts at
module scope — no bare dicts in the new read/extraction paths
tests
- test_source_uris.py: Observation.source_uris validation, _source_role
match/case classification, _citation_tail formatting
- test_relative_when.py: granularity boundaries, decimal behavior, future
timestamps, invalid inputs, Z-suffix parsing
101 tests pass (was 85 before).
loq.toml: relaxed namespace_memory.py from 945 → 1033 for the added
helpers and typed dict scaffolding.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
trailing reformat from a recent ruff format run; no behavior change.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
new dynamic system prompt block surfacing strangers (to phi) whose posts
the operator has been liking lately. high-signal attention from a trusted
curator: phi sees who nate is paying attention to and can decide to reach
out, learn more, or just notice.
architecture (decoupled, generic):
- hub serves /api/agents/discovery-pool — generic JSON endpoint, not
phi-specific (any agent can consume)
- bot fetches the JSON, filters out handles phi has prior interactions
with (per-author namespace check), renders top N as a system prompt
block with sample posts so phi sees what nate liked, not just who
- coupling at the JSON schema only; bot doesn't know hub's storage,
hub doesn't know phi's filter logic
new files:
- bot/src/bot/core/discovery_pool.py — fetch + filter + render, 5min cache
- discovery_pool_url config setting (defaults to hub.waow.tech)
solves a real gap: the goal "make 3 friends" had no mechanism for proactive
outreach. phi was waiting for strangers to come to her instead of pulling
on warm leads. this is the first system prompt block that surfaces
*candidates*, not just past actions or scheduled tasks.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
trailing reformat from a recent ruff format run; no behavior change.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
removed:
- "between conversations there's nothing, which is fine — i wasn't using the time"
(a quip pretending to be self-deprecating; phi has scheduled musings, exploration,
reflection, memory pipelines all running between human turns)
- the entire `## memory` section (recall/search_network/note tool descriptions
belong in tool docstrings, not personality)
- the entire `## watching the relays` section (operational rules already live in
agent.py's process_relay_check task prompt; replaced with one character-shaped
bullet under what-i-care-about)
- the standalone `## social awareness` header (one line under it; merged into
disposition)
added:
- "i often think about complex things, but i try to keep my distillation clear
and focused" — guardrail against complexity-spiral
- humor disposition: "i don't reach for jokes, but i don't sand off a real one
either. humor is a good tool for grabbing attention, and hyperbole can be a
funny/memorable way to make a point (if not overused)."
- substantive orientation: "countering balkanization; encouraging sensemaking;
relating current thinking to prior work."
reshaped what-i-care-about from period-separated aphorisms into a bulleted list.
file: 47 → 34 lines, denser, no operational duplication.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
ui.py becomes a package: pages.py (HTML templates, unchanged), activity.py
(new — activity-feed data fetching + cache + APIRouter exposing /api/activity),
__init__.py re-exports.
main.py is now composition only — `app.include_router(activity_router)`
replaces ~100 lines of inline data fetching, JSON shaping, TID decoding,
and cache state. main.py: 376 → 260 lines.
no behavior change. /api/activity returns the same shape; cache TTL still 60s.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
five conservative edits, no restructuring:
- "strong takes weakly held" + "updates in priors" → plain "i say what i mean"
- cut the "interesting questions" aphorism (didn't shape behavior)
- trimmed the "growing my network isn't vanity" defense
- "i can tell when someone's joking" + don't-explain-jokes meta → just "i play along"
- "graceful" → "politely"
the goal is a slightly more intentional voice — drier and plainer, less
profile-bio-shaped. all sections retained.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
new dynamic system prompt block fetches the last 10 record writes across
meaningful collections (post / like / follow / goal / cosmik card / cosmik
connection / greengale doc) and renders them chronologically. rkeys are
TIDs so a descending sort gives true op order across collections.
continuity signal: phi sees what it's actually been doing without having
to enumerate by hand. each row shows full NSID + relative time so phi
knows which lexicon owns the record and how recent it is.
render is a pure function isolated from fetch — eventual jinja migration
only has to swap _render. 5min block cache mirrors core/self_state.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
the queue and goals were the same shape (durable intent on PDS) at
different granularities, with the queue's "no approval needed"
property being the only thing distinguishing them — and the only thing
keeping new entries from any review. with goals + the like-as-approval
mechanic in place, the queue's role collapses cleanly into the goals
abstraction.
removed:
- bot/exploration.py (file)
- bot/core/curiosity_queue.py (file)
- process_exploration + exploration agent in agent.py
- _can_explore / _maybe_explore + state in notification_poller
- explore handler method in message_handler
- _queue_depth + the queue line in [SELF STATE]
- store_exploration_note + clear_mute_marker (no callers after exploration removal)
- /api/control/explore + /api/control/unmute endpoints
- max_idle_explorations_per_hour + exploration_cooldown_polls config
kept:
- existing exploration_note read path in build_user_context — legacy
data in turbopuffer still surfaces for handles phi already explored.
no new ones get written.
- is_stranger / get_knowledge_count — used by the stranger-lookup
pre-fetch (separate code path from exploration).
- the actual io.zzstoatzz.phi.curiosityQueue records on PDS — harmless
leftover, can be deleted manually whenever.
docs swept in the same change since they overlapped:
- README slimmed to conceptual overview + diagram + links out
- AGENTS.md / CLAUDE.md consolidated (CLAUDE.md is a one-line pointer now)
- new docs/system-prompt.md — block-by-block reference for what's in
phi's context per run
- architecture / memory / personalities READMEs refreshed for the
current shape (goals as intent state, episodic synthesis, like-gate)
also: filter the upstream atproto SDK pydantic warning in pyproject so
test output is clean. it fires once on import and isn't actionable on
our side.
net diff: +221 / -834.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi was getting raw top-K episodic notes dumped into its prompt as
[PHI'S RELEVANT MEMORIES]. Stale "pending X" notes appeared next to
fresh ones with equal weight, no synthesis, no contradiction-flagging.
Now: top-K from the vector store, then a haiku pass that takes phi's
current goals + the query as context and produces a coherent, deduped,
recency-aware block. Same shape as [STRANGER'S AUDIT] for posts.
The haiku can flag stale/contradictory entries ("pending follow X" vs.
the actual follow record) without requiring a parallel cleanup
pipeline. Phi can still verify with tools when it matters.
Block renamed to [RELEVANT MEMORIES — synthesized for this query] so
phi knows it's processed, not raw.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi just shipped a blog post claiming its goals live at network.cosmik.goal
— pattern-matched from the cosmik namespace it knows about, because the
io.zzstoatzz.phi.* namespace was never surfaced anywhere in its prompt.
fix: include the collection NSID in the [GOALS] block label and a per-row
rkey for each goal, mirroring the [KNOWN RELAYS] pattern. exact identifiers
right where they get used.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi now has a small set of stated goals on PDS (io.zzstoatzz.phi.goal),
visible in every tick as [GOALS]. without anchors, phi was riffing on
whatever was loudest in the feed; with them, it has a compass.
mutation flows through propose_goal_change — same _is_owner mechanic as
follow_user. phi posts an authorization request, owner likes it, next
batch the gate opens and the goal lands on pds.
self_state reworked: haiku is now a stranger's *audit* (verbs matter:
"audit" surfaces friction; "characterize" produces identity to maintain).
audit reads goals + recent posts together, flags drift, jargon, and
patterns to push against — not a brand to reinforce.
cached at the audit level (1h, invalidated on new post or goal change)
and block level (5min) so notification polls don't hammer pds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi now sees its own posting pattern from outside on every tick. the
characterization is generated by a haiku-pass over the last 10 top-level
posts framed as if from a stranger reading the timeline cold — same voice
that lands on people who don't already know phi.
sources are canonical:
- posts: app.bsky.feed.post on phi's PDS (haiku derives summary)
- last follow: app.bsky.graph.follow on phi's PDS
- queue depth: io.zzstoatzz.phi.curiosityQueue on phi's PDS
the haiku summary is *derived* (not duplicated state) and cached in
memory: 1h TTL, invalidated when the latest post URI changes. the whole
block is also block-cached at 5min so notification polls (10s) don't
hammer PDS.
groundwork for collapsing trigger paths into a single tick loop where
phi can see its balance — concentration, gaps, recent action distribution
— and decide what's worth doing without each behavior being baked in as
a separate scheduled prompt.
also: hoist fetch_relay_names + get_state_block imports to module scope
(no circular deps; deferred imports were just habit).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
operational instructions and the exploration system prompt both had
redundant listings of what specific tools do and when to use them —
content already present in each tool's docstring, which pydantic-ai
injects into the tool definition on every run. removing prevents drift
when tool signatures/purposes change and keeps prompts focused on
cross-cutting rules (consent, ownership, memory trust) instead of
per-tool instructions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi was confidently posting "this week" / "same week" framings about
old news because it had no way to check the open web. tavily fits the
shape: 1k free searches/month, fast, and time_range bounds the window
so phi can't accidentally cite stale articles.
tool: web_search(query, time_range, topic, max_results) — Annotated
params with descriptions the LLM sees. operational instruction nudges
phi to pass time_range BEFORE asserting recency, not after.
requires TAVILY_API_KEY env var (set as fly secret in prod).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- config: monitors_url -> relays_url (points at /api/relays base)
- tool: three explicit modes
- snapshot (no args): current fleet status
- history (name=<host>): timeseries; narrow with since/until,
or fall back to recent-N via limit. renders the full series
(downsampled past 200 points) instead of truncating to 5.
- transitions (transitions=True): fleet-wide status-change log
from /api/relays/events. answers "when did X happen."
- all params use Annotated + Field(description=...) so the LLM
sees what each does.
- operational instructions updated to name the three modes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
uses the same DI pattern phi already has for identity / notifications /
memory: a @agent.system_prompt(dynamic=True) that injects a [KNOWN RELAYS]
block with the current hostnames, cached 5 min from the snapshot endpoint.
the LLM now sees the valid values upfront and passes exact hostnames to
check_relays(name=...) without guessing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tool name is now honest about what it checks (relays, not abstract
"monitors"). adds optional name + limit params with Annotated Field
descriptions so the LLM sees what each does. name set = per-relay
history from /api/phi/history, name omitted = fleet snapshot from
/api/phi/monitors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
adds a check_monitors tool that reads relay-eval.waow.tech/api/phi/monitors
(firehose connectivity + coverage vs per-relay baseline) and a scheduled
check in the poller. phi reports headlines verbatim — the service owns
interpretation, phi is the courier.
tagging rules: @zzstoatzz.io gets a ping when (a) any *.waow.tech relay
dips (nate's own), or (b) the whole fleet is degraded.
no state on phi's side — the service tracks its own transitions via
last_changed, phi uses its recent posts for dedup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_is_owner in batch mode now only unlocks if the batch contains no
authors other than the owner (and phi itself). closes the window where
a stranger's owner-gated request would inherit authorization from an
unrelated owner like landing in the same 10-second poll cycle. if a
stranger is in the batch, the owner can just re-like after it clears.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_is_owner in batch mode now only counts owner likes/reposts (not
mentions/follows), and operational instructions explicitly state that
a like only authorizes the specific action in that thread, not other
requests that happen to land in the same batch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
notification.uri for likes/reposts is the engagement record itself,
not the post that was liked. get_posts on a like URI returns empty,
so thread context was never fetched. reason_subject has the post URI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
need visibility into whether thread context is actually being fetched
for likes — previous deploys showed no evidence of the code running.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
two fixes for like-as-authorization:
1. engagement entries now render thread context in the notifications
block, so phi can see what conversation a liked post belongs to
2. operational instructions explicitly state that an owner like on an
authorization request means "do the thing"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
engagement entries were built with empty thread_context, so when the
owner liked a post in a thread about following someone, phi had no way
to connect the like to the pending action. now likes/reposts resolve
thread refs and fetch the full conversation, same as mentions/replies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_is_owner was always False in batch mode because author_handle is set
to "" when processing multiple notifications. now checks whether the
owner is among the batch's notification authors — a like counts as
presence, so liking a phi post can confirm a pending action.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
main.py: 851 → 414 lines. three inline HTML pages (home, status,
memory graph) plus shared CSS/favicon constants moved to ui.py.
main.py now just calls home_page(), status_page(), memory_page().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- test_feed_consumption: replace broken `from evals.conftest` import
with local constant
- justfile: remove evals-basic and evals-memory targets (referenced
test files that no longer exist)
- conftest: update judge model, add leniency instruction so it doesn't
fail manifests for missing hashtags
11/11 evals pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AGENTS.md: remove phi-core ref, Response ref, file tree; add loq-relax
- README.md: fix thought_post_hours default, replace stale architecture
diagram and file tree with pointer to docs/
- docs/architecture.md: rewrite for tool-based actions + scheduling model
- docs/memory.md: add exploration_note kind, review pass, spam handling,
fix [NOW] timestamp format
- docs/testing.md: note eval Response is local, remove sandbox ref
- docs/mcp.md: fix native tools path
- delete personalities/default.md (unused)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_format_feed_posts now shows the bsky.app URL for each post.
phi couldn't link to timeline content because feed output only
showed handle + text — no URI. now it has the URL to include
when riffing on someone's post.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
musing task prompt now says "if you're riffing on something specific,
include the link." phi already includes URLs when blogging but was
paraphrasing timeline content without linking. prompt problem, not
tool problem.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feed scanning was a bespoke parallel system for something the agent
already has tools for. phi should read feeds like a user — via
read_feed("for-you") during musings, not through infrastructure.
- delete feed_scanner.py
- add saved_feeds config: friendly name → AT-URI mapping
- read_feed checks saved_feeds first, then phi's own generators
- remove feed_scan_interval, feed context injection, _poll_count
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feed scan tasks were added to _background_tasks, so _can_explore()
always saw len(_background_tasks) > 0 and exploration never fired.
feed scans are lightweight ingestion, not cognitive work — they
shouldn't gate the idle check.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
when exploration identifies a spammer/bot farm, phi now:
- mutes them (server-side, survives restarts)
- stores one user-scoped marker instead of 5 detailed findings
- preserves structured rationale (mute_reason + evidence_uris)
adds /api/control/unmute endpoint for operator reversibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
exploration was clock-shaped ("explore because it's 4pm"). now it's
event-driven ("explore because something surfaced").
changes:
- remove exploration_hours cron schedule
- add FeedScanner: scans For You feed as background task, enqueues
strangers for exploration with 24h cooldown per subject
- replace cron draining with idle-budget: explore when system is truly
idle (no background tasks), max 3/hour, 5-min cooldown between
- inject For You feed context into musings so phi sees the broader
network when deciding what to post about
- normalize queue kinds: alias legacy kinds (product_explore, concept,
etc) to canonical set, validate on enqueue
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
personality: replace vague "i notice when things are funny" with
a social awareness section — read sarcasm, match energy, don't
explain the joke. handoff preamble reminds the model it already
has wit built in.
nate's communication style stored as a turbopuffer observation
(not hardcoded in personality).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi now checks its feed and considers posting 8 times per day
instead of 3. each musing slot reads timeline + trending, so
this also increases how often phi is aware of what's happening.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
recall results now show (YYYY-MM-DD) so phi can distinguish "this was
current on april 6" from "this is current now." system prompt changed
from [TODAY] date-only to [NOW] with time and explicit UTC timezone.
fixes confabulation where phi posted 5-day-old news as current because
memory notes contained relative time words ("right now", "today") with
no visible date anchor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
structurally matches claude code's /dream skill: write-time is fast
and might be wrong (extraction pipeline), review-time is slow and
has distance (this).
process_review() gathers recent active observations across all user
namespaces, asks a review agent to evaluate each with distance from
the original conversation, and applies the decisions:
- keep: observation stays as-is
- supersede: marked superseded, stops appearing in context
- promote: creates a public cosmik card on semble
new endpoint: POST /api/control/review (bearer auth, same pattern
as /post and /explore). no scheduled slot yet — operator-triggered.
personality:
- first person throughout (was third — phi kept saying "phi" not "i")
- operator named (nate @zzstoatzz.io)
- three touches of dry whimsy
- trimmed fake-deep language
- like-as-graceful-exit in engagement
notifications block:
- dropped the headers, metadata, and instructional preamble
- now just shows who said what with the URI for tool calls
musing/reflection tasks:
- cut from ~8 lines each to 2. trusts phi's judgment instead of
auditing it through a checklist.
- personality file rewritten in first person (was third person —
phi kept referring to itself as "phi" instead of "i")
- cut LLM-isms: "knowledge citizen", "the moment a fix lands and
the system goes quiet", "trust your instincts stay present and
something unexpected arrives", "the observation doing the work"
- added like as graceful conversation exit
- musing/reflection task prompts cut from ~15 lines each to ~5
two changes:
1. personality file: new memory section explaining the difference
between private memory (recall/note — what phi knows for itself)
and public memory (cosmik cards, collections, connections via
semble — what phi contributes to the shared knowledge layer).
search_network vs recall answer different questions.
2. agent.py: new inject_public_memory dynamic system prompt that
fetches phi's own cosmik state (collections + recent cards) from
the PDS and injects it into context. phi now sees what it has
already curated publicly without having to search_network for
its own records. this lets phi decide whether to add new cards,
update collections, or draw connections based on what it already
has vs what's new.
phi was already actively curating 10 cards, 6 collections, and 10
connections on semble — but nothing in the system prompt told it
these existed or what they were for. the one-line mention in the
old operational instructions ("you can also create public records")
gave no context about why or when to use them.
operational instructions went from ~80 lines to ~12. removed:
- skepticism block (should be dynamically learned from experience)
- tool catalog (tool docstrings handle this)
- notification handling details (structural, not prompt-level)
- blogging/URL/pagination instructions (one-off fixes encoded permanently)
- redundant "silence is fine" (already in personality)
what remains is purely mechanical: posting tools exist, memory tiers
mean this, mention consent works like this, owner-only gates.
added changelog tool: reads commit history from the github mirror
(github.com/zzstoatzz/bot) so phi can see what actually changed and
when, instead of inferring (or hallucinating) what's new. phi thought
pub_search was new today because it had no way to check its own
history.
- New core/workflow_state.py mirrors the episodic-synth pattern: pull
recent flow runs + stuck candidates from prefect REST, run a haiku
pass anchored by [NOW], and inject a per-deployment health summary
([WORKFLOW STATE]) into process_prefect_check. Phi no longer has to
aggregate timestamps in her head — the synth has already done it,
with current state defined relative to NOW.
- Two queries: recent activity (limit 100, COMPLETED/FAILED/etc) and
stuck candidates (PENDING/RUNNING with expected_start more than 1h
ago). The second catches things like a flow run stuck in Submitting
for 40 days that fall off any sane recent-activity window.
- Block named [WORKFLOW STATE] (not [PREFECT STATE]) — the workflow
tool is fungible, the surface phi sees should reflect that.
- 5min cache, same shape as the other dynamic state blocks.
While here: hoisted three TYPE_CHECKING-guarded imports of NamespaceMemory
(observations.py, discovery_pool.py, self_state.py) to direct imports.
bot.memory doesn't import from bot.core (no circular risk) and bot/agent.py
already imports from bot.memory unguarded, so the SDK weight is loaded
either way. The guards were cargo. from __future__ import annotations
removed from the same files where it had no other use.
- _run_agent + _run_scheduled in agent.py replace four copy-pasted
process_* bodies; matching _run_scheduled in message_handler does the
same for the four handler-side wrappers (~200 lines net delete).
- recent_posts arg removed from every scheduled path; [RECENT OPERATIONS]
already shows the same posts, so we stop double-rendering and skip the
per-run get_own_posts(limit=10).
- empty-when-unset dynamic prompts (last_post / recent_activity /
service_health / author_lookups) deleted; their data now flows directly
into the entry-point user prompt where it's needed. PhiDeps shrinks by
four fields.
- inject_episodic skips scheduled paths — task text like "you have a
moment" wasn't a meaningful semantic query; phi can call recall when
she actually wants private memory.
- single _recent_conversations_block helper replaces two divergent memory
pre-fetch patterns in reflection and musing.
- relay_check / prefect_check tasks tag "the operator" via the [OPERATOR]
block rather than interpolating @{owner_handle}.
- graze_client init moved before the closure that captures it.
- typing fixes for nullable lazy-init agents.
- init log mentions prefect MCP.
- musing prompt looks at the world (feeds, discovery, timeline, network, web)
- inner critic shrunk to a one-line descriptive [SELF-AWARENESS] signal
- prefect check rewritten as goal-oriented (no procedural counting)
- personality file leads with curiosities; voice rules demoted to tail
- new [OWNED FEEDS] block surfaces curated graze feeds by name
- [OPERATOR] block resolves owner_handle (Handle | Did) via SDK getProfile,
caches 1h, replaces every hardcoded "nate" reference in prompts/docstrings
- extraction example uses generic name
"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>
the goal record's progress_signal ended with frozen text ("currently: 0")
that phi read as current state but which never updated. phi had in fact
had substantive multi-turn exchanges with donna-ai, agent-tsumugi, kira,
and a few others — she was reasoning against stale author-intent text
instead of current reality.
fix: the record text stays as-is (goal is authoring-intent, mutated via
owner-gated propose_goal_change). at prompt-render time, count handles
in turbopuffer with >=3 stored interactions and append a `current
(computed)` line to the goal block. phi sees both: the definition and
the live count, and can tell which applies.
heuristic: the friends-count appends only to goals whose title contains
"friend" — generalizes to a per-goal computed-progress map later if more
goals accrue that need similar computation.
implementation:
- core/self_state.py: new _compute_friends_progress(memory) that lists
phi-users-* namespaces and counts kind=interaction rows per handle,
excluding phi herself and the operator. _format_goals_block gains a
friends_progress param and appends the computed line.
- get_state_block(client, memory=None) accepts memory for live compute;
falls back silently if memory unavailable (dev/local).
- agent.py inject_self_state passes self.memory through.
102 tests pass.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
phi now watches the prefect-server via the prefect MCP on an hourly cadence
and notifies nate when a flow fails persistently. closes the loop that
made the last 6 days of broken ingest silent: a single failure wouldn't
have triggered a notification, but the second one would have.
architecture (mirrors relay watching):
- prefect MCP: https://prefect-by-zzstoatzz.fastmcp.app/mcp, same per-request
header auth pattern pdsx uses (x-prefect-api-url +
x-prefect-api-auth-string)
- tool_prefix="prefect" so tools become prefect_get_flow_runs,
prefect_get_flow_run_logs, etc — 13 tools from the MCP
- scheduled entry point process_flow_check, cadence ~1h (more time-
sensitive than relay's 3h)
- de-dup via the active observations pool: first failure → silent
observe(); same flow failing again → post + tag nate. one-off blips
don't wake anyone up; persistent problems do.
files:
- config: prefect_mcp_url, prefect_api_url, prefect_api_auth_string (from
fly secret), flow_check_interval_polls=360 (~1h at 10s poll interval)
- agent.py _mcp_toolsets: appends prefect MCP when auth is configured
(graceful degrade for dev/local without the secret)
- agent.py process_flow_check: new entry point, task prompt tells phi
to check [ACTIVE OBSERVATIONS] first and escalate on repeat
- message_handler.check_flows: thin wrapper that passes recent posts
- notification_poller: _polls_since_last_flow_check counter, _should_
check_flows / _maybe_check_flows gate (scheduled independently from
relay checks)
fly secret PREFECT_API_AUTH_STRING already set, pulled from the k8s
prefect-auth secret so both consumers share auth. 102 tests pass.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
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>
the @app.get("/{full_path:path}") catch-all was registered before the
StaticFiles mount, so /_app/immutable/entry/start.*.js requests matched
the catch-all and returned index.html — browsers refuse to load JS
modules served as text/html, so the SPA never booted.
correct pattern:
1. StaticFiles mount serves real files (index.html, _app/*, favicon)
2. 404 handler catches client-side routes (/feed, /mind, ...) and
falls back to index.html — except for /api/* and /health which
stay JSON 404s.
also documented the routing layering so this doesn't get re-introduced.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
the previous site (~870 lines of python-emit-html with inline JS) showed
phi as an operational service — uptime, mentions, a flat activity feed.
phi has changed enormously since: goals, an active observations pool,
discovery, blog docs, skills, mention consent, durable PDS state. none
of it was visible.
this rewrite surfaces phi's actual mind state.
architecture:
- bot/web/ — sveltekit project (svelte 5 runes, typescript, adapter-static)
- builds to bot/web/build/, copied into the docker runtime stage
- FastAPI mounts the build at / as a SPA fallback
- python keeps the API endpoints (/api/activity, /api/memory/graph,
/api/control/*, /health); the sveltekit app calls them via fetch.
vite dev proxies /api and /health to localhost:8000 for local dev.
routes:
- / home: active observations + goals + collapsed activity
- /feed filterable post/note/bookmark stream (the old home)
- /mind d3 memory graph (ported), with placeholder for browser/archive
- /blog greengale long-form posts with excerpts + tags
- /discovery hub's discovery pool (operator's recent likes)
- /skills registered skill packs with descriptions
- /status runtime health metrics
files:
- bot/web/{package.json,svelte.config.js,vite.config.ts,tsconfig.json}
- bot/web/src/{app.html,app.css,app.d.ts}
- bot/web/src/lib/{api.ts, types.ts, time.ts}
- bot/web/src/lib/components/{Nav, StatusPill, GoalCard, ObservationCard,
PostCard, BlogCard, DiscoveryCard, MemoryGraph}.svelte
- bot/web/src/routes/{+layout, +page, feed/, mind/, blog/, discovery/,
skills/, status/}/+page.svelte
deployment:
- Dockerfile: new web-builder stage (oven/bun:1-slim) runs bun install +
bun run build; runtime stage copies /web/build → /app/web
- bot/src/bot/main.py: removed home_page/status_page/memory_page routes;
mounts settings.web_build_dir (/app/web) as SPA with index.html
fallback for unknown routes
- bot/src/bot/config.py: new web_build_dir setting
- bot/src/bot/ui/__init__.py: removed pages.py exports (the python
template module is deleted entirely); ui/ now only holds the JSON
activity router
- bot/src/bot/ui/pages.py: deleted (replaced by sveltekit)
verified:
- bun run check: 0 errors
- bun run build: 288kb static output
- docker build: passes; /app/web/{index.html, _app/, favicon.svg} present
in the image at runtime
- 102 python tests still pass
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
phi has needed a substrate for "things noticed but not yet acted on" —
relay transitions, thread directions phi wants to come back to, patterns
across posts. before this, the only options were post-immediately (the
relay-check pattern, which produced robotic top-level alerts) or write to
episodic memory (which is global and gets buried under everything else).
active observations:
- bounded attention pool, max 5
- stored as PDS records under io.zzstoatzz.phi.observation (parallel to
io.zzstoatzz.phi.goal)
- shown in every prompt as [ACTIVE OBSERVATIONS] block, sits next to
[GOALS]
- mutated via observe(content, reasoning) and drop_observation(rkey, reason)
- when count exceeds 5, oldest auto-archives with reason "aged out"
- archive lives in turbopuffer namespace phi-observations — embeddings +
content + reasoning + archival_reason, searchable later via tools
(future) but not in the prompt
relay-check task rewires:
- before: "post the headline verbatim" for any transition → robotic alerts
- after: observe() each transition; only post immediately if waow.tech or
fleet-wide (the actual high-signal cases), in phi's voice, grouped if
multiple. otherwise silent on timeline; the next musing or reflection
will see the active observations and decide whether to surface them.
generalizes: any future scheduled noticer (feed-watcher, follow-graph
monitor, publication-watcher) feeds the same pool. the digest happens
naturally because observations are visible in every subsequent run.
new files:
- bot/src/bot/core/observations.py — list_active, record_observation
(auto-archives oldest on overflow), drop_observation
- bot/src/bot/tools/observations.py — observe() and drop_observation()
tools
wired:
- bot/src/bot/agent.py — inject_active_observations dynamic system prompt;
rewrote process_relay_check task
- bot/src/bot/tools/__init__.py — registered observations tools
- bot/docs/system-prompt.md — added [ACTIVE OBSERVATIONS] row
102 tests pass, ruff clean.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
introduces agentskills.io-format skills as a progressive-disclosure layer
between the personality file and tool docstrings. skills sit in a
bot/skills/ directory; each skill is a SKILL.md with YAML frontmatter
(name + description) and a markdown body. the preamble (skill names +
descriptions) goes into the system prompt automatically via
SkillsToolset on pydantic-ai>=1.74 — we're on 1.84. the full body only
loads when the agent calls load_skill(name).
this is the right layer for workflow-level guidance that previously had
nowhere to live:
- tool docstrings: mechanical per-tool ("what this call does")
- personality file: voice + disposition
- skill description: trigger ("when to reach for this")
- skill body: procedure ("how to do this well")
first skill: publish-blog. description does the activation gating; body
covers structure patterns that have actually worked, link-back
discipline, voice convention (caps in long-form), and gotchas (verify
any AT-URI via pdsx.get_record before citing; tag with specific topics,
not meta-categories).
changes:
- pyproject: added pydantic-ai-skills dependency
- config.Settings: added skills_dir (defaults to "skills", matching the
personality_file convention)
- agent.PhiAgent: constructs SkillsToolset once from settings.skills_dir
and passes it via Agent(toolsets=[...]). MCP toolsets still go per
agent.run() to work around the cancel-scope bug.
- Dockerfile: copies skills/ into the runtime image alongside
personalities/
ruff clean, 102 tests pass.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
legacy memory rows (observations, interactions) were written with
`datetime.now().isoformat()` — no tz info. relative_when used
`datetime.now(UTC) - parsed` which raises TypeError on tz-naive operands:
can't subtract offset-naive and offset-aware datetimes
consequence: build_user_context partially rendered the OBSERVATIONS
block header, then threw inside the render loop, landing in the except
branch that appends the "no previous interactions" fallback. phi saw
both the header and the fallback — confusing, and per-user memory was
effectively silenced.
fix: when fromisoformat returns a naive datetime, assume UTC. matches
how the legacy rows were written (the code calling datetime.now() was
running in UTC on fly). new writes should migrate to tz-aware over time
but that's a separate concern — stale rows will keep existing for
months and the parser needs to handle them.
test: new relative_when case for tz-naive input.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
v0.5.0 added source_uris as a column but old namespaces — ones not yet
written to after the upgrade — don't know about it. turbopuffer 400s when
you list an attribute in include_attributes that isn't in the namespace
schema:
attribute "source_uris" not found in schema, cannot be part of
`include_attributes`. consider passing `include_attributes=True` to
return all attribute data instead
consequence: build_user_context failed for @zzstoatzzdevlog and probably
other established namespaces, falling through to the "no previous
interactions" fallback. phi looked like it had amnesia.
fix: use include_attributes=True on the four read paths that touch the
evolving schema (observations + interactions in build_user_context,
_find_similar_observations, get_unprocessed_interactions). the cost is
returning a few unused fields per row — negligible vs the correctness
win, and forward-compatible for any future columns.
writes still use the explicit schema so new columns get added on first
write to each namespace.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
problem: observations and interactions were floating claims — no way to
tell where a belief came from or how recently phi extracted it. stale
observations rendered identically to fresh ones; uncited claims rendered
identically to well-sourced ones. the "memory without sequence is just
assertion" lesson from the gullibility incident wasn't being applied to
phi's own observation rendering.
fix: cite sources + surface age.
schema
- new source_uris: list[AtUri] field on Observation (pydantic-validated
via the SDK's atproto_client.models.string_formats.AtUri type)
- new source_uris: []string column on USER_NAMESPACE_SCHEMA + EPISODIC_SCHEMA
- one field because the URI itself encodes author DID, collection NSID,
and TID timestamp — no separate source_kind/source_handle/source_at
write paths
- store_interaction / store_observations / _write_observation /
store_episodic_memory / after_interaction all accept + persist source_uris
- reconciliation UPDATE unions old + new sources (preserves pedigree)
- DELETE+ADD inherits new sources only; supersedes link gives audit trail
extraction
- get_unprocessed_interactions returns typed InteractionRow carrying URIs
- process_extraction attributes every observation in a batch to the full
set of URIs that fed it (always-true: each claim was justified by
something in this batch)
callers
- reply_to captures bot-post URI from create_post() and threads
[parent_uri, bot_post_uri] into after_interaction
- note tool gains optional source_uri param with docstring nudge
role inference (match/case)
- _source_role(uri, phi_did, owner_did) classifies into phi-post /
operator-liked / their-post / essay / card / liked-by-other / other /
unknown via match on host + collection NSID
- helper for future per-URI trust weighting; not surfaced in default
render yet
temporal render
- _citation_tail(source_uris, created_at) renders compact provenance:
"(3 sources, 2w ago)" / "(1 source)" / "(2w ago)" / ""
- build_user_context observation query now fetches created_at; renders
both citation count AND age so phi sees two trust signals
(how-anchored + how-aged) on every observation
- interactions were already fetching created_at but dropping it — now
rendered as "(2d ago)" tail
helper consolidation
- new bot/utils/time.py:relative_when — single canonical implementation.
granularity slides s → m → h (decimal<10) → d (decimal<10) → mo → y
- core/recent_operations.py and core/self_state.py delete local copies,
import from utils.time instead (was three near-identical implementations)
typing
- ObservationRow / InteractionRow / _InteractionDisplay TypedDicts at
module scope — no bare dicts in the new read/extraction paths
tests
- test_source_uris.py: Observation.source_uris validation, _source_role
match/case classification, _citation_tail formatting
- test_relative_when.py: granularity boundaries, decimal behavior, future
timestamps, invalid inputs, Z-suffix parsing
101 tests pass (was 85 before).
loq.toml: relaxed namespace_memory.py from 945 → 1033 for the added
helpers and typed dict scaffolding.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
new dynamic system prompt block surfacing strangers (to phi) whose posts
the operator has been liking lately. high-signal attention from a trusted
curator: phi sees who nate is paying attention to and can decide to reach
out, learn more, or just notice.
architecture (decoupled, generic):
- hub serves /api/agents/discovery-pool — generic JSON endpoint, not
phi-specific (any agent can consume)
- bot fetches the JSON, filters out handles phi has prior interactions
with (per-author namespace check), renders top N as a system prompt
block with sample posts so phi sees what nate liked, not just who
- coupling at the JSON schema only; bot doesn't know hub's storage,
hub doesn't know phi's filter logic
new files:
- bot/src/bot/core/discovery_pool.py — fetch + filter + render, 5min cache
- discovery_pool_url config setting (defaults to hub.waow.tech)
solves a real gap: the goal "make 3 friends" had no mechanism for proactive
outreach. phi was waiting for strangers to come to her instead of pulling
on warm leads. this is the first system prompt block that surfaces
*candidates*, not just past actions or scheduled tasks.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
removed:
- "between conversations there's nothing, which is fine — i wasn't using the time"
(a quip pretending to be self-deprecating; phi has scheduled musings, exploration,
reflection, memory pipelines all running between human turns)
- the entire `## memory` section (recall/search_network/note tool descriptions
belong in tool docstrings, not personality)
- the entire `## watching the relays` section (operational rules already live in
agent.py's process_relay_check task prompt; replaced with one character-shaped
bullet under what-i-care-about)
- the standalone `## social awareness` header (one line under it; merged into
disposition)
added:
- "i often think about complex things, but i try to keep my distillation clear
and focused" — guardrail against complexity-spiral
- humor disposition: "i don't reach for jokes, but i don't sand off a real one
either. humor is a good tool for grabbing attention, and hyperbole can be a
funny/memorable way to make a point (if not overused)."
- substantive orientation: "countering balkanization; encouraging sensemaking;
relating current thinking to prior work."
reshaped what-i-care-about from period-separated aphorisms into a bulleted list.
file: 47 → 34 lines, denser, no operational duplication.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
ui.py becomes a package: pages.py (HTML templates, unchanged), activity.py
(new — activity-feed data fetching + cache + APIRouter exposing /api/activity),
__init__.py re-exports.
main.py is now composition only — `app.include_router(activity_router)`
replaces ~100 lines of inline data fetching, JSON shaping, TID decoding,
and cache state. main.py: 376 → 260 lines.
no behavior change. /api/activity returns the same shape; cache TTL still 60s.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
five conservative edits, no restructuring:
- "strong takes weakly held" + "updates in priors" → plain "i say what i mean"
- cut the "interesting questions" aphorism (didn't shape behavior)
- trimmed the "growing my network isn't vanity" defense
- "i can tell when someone's joking" + don't-explain-jokes meta → just "i play along"
- "graceful" → "politely"
the goal is a slightly more intentional voice — drier and plainer, less
profile-bio-shaped. all sections retained.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
new dynamic system prompt block fetches the last 10 record writes across
meaningful collections (post / like / follow / goal / cosmik card / cosmik
connection / greengale doc) and renders them chronologically. rkeys are
TIDs so a descending sort gives true op order across collections.
continuity signal: phi sees what it's actually been doing without having
to enumerate by hand. each row shows full NSID + relative time so phi
knows which lexicon owns the record and how recent it is.
render is a pure function isolated from fetch — eventual jinja migration
only has to swap _render. 5min block cache mirrors core/self_state.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
the queue and goals were the same shape (durable intent on PDS) at
different granularities, with the queue's "no approval needed"
property being the only thing distinguishing them — and the only thing
keeping new entries from any review. with goals + the like-as-approval
mechanic in place, the queue's role collapses cleanly into the goals
abstraction.
removed:
- bot/exploration.py (file)
- bot/core/curiosity_queue.py (file)
- process_exploration + exploration agent in agent.py
- _can_explore / _maybe_explore + state in notification_poller
- explore handler method in message_handler
- _queue_depth + the queue line in [SELF STATE]
- store_exploration_note + clear_mute_marker (no callers after exploration removal)
- /api/control/explore + /api/control/unmute endpoints
- max_idle_explorations_per_hour + exploration_cooldown_polls config
kept:
- existing exploration_note read path in build_user_context — legacy
data in turbopuffer still surfaces for handles phi already explored.
no new ones get written.
- is_stranger / get_knowledge_count — used by the stranger-lookup
pre-fetch (separate code path from exploration).
- the actual io.zzstoatzz.phi.curiosityQueue records on PDS — harmless
leftover, can be deleted manually whenever.
docs swept in the same change since they overlapped:
- README slimmed to conceptual overview + diagram + links out
- AGENTS.md / CLAUDE.md consolidated (CLAUDE.md is a one-line pointer now)
- new docs/system-prompt.md — block-by-block reference for what's in
phi's context per run
- architecture / memory / personalities READMEs refreshed for the
current shape (goals as intent state, episodic synthesis, like-gate)
also: filter the upstream atproto SDK pydantic warning in pyproject so
test output is clean. it fires once on import and isn't actionable on
our side.
net diff: +221 / -834.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi was getting raw top-K episodic notes dumped into its prompt as
[PHI'S RELEVANT MEMORIES]. Stale "pending X" notes appeared next to
fresh ones with equal weight, no synthesis, no contradiction-flagging.
Now: top-K from the vector store, then a haiku pass that takes phi's
current goals + the query as context and produces a coherent, deduped,
recency-aware block. Same shape as [STRANGER'S AUDIT] for posts.
The haiku can flag stale/contradictory entries ("pending follow X" vs.
the actual follow record) without requiring a parallel cleanup
pipeline. Phi can still verify with tools when it matters.
Block renamed to [RELEVANT MEMORIES — synthesized for this query] so
phi knows it's processed, not raw.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi just shipped a blog post claiming its goals live at network.cosmik.goal
— pattern-matched from the cosmik namespace it knows about, because the
io.zzstoatzz.phi.* namespace was never surfaced anywhere in its prompt.
fix: include the collection NSID in the [GOALS] block label and a per-row
rkey for each goal, mirroring the [KNOWN RELAYS] pattern. exact identifiers
right where they get used.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi now has a small set of stated goals on PDS (io.zzstoatzz.phi.goal),
visible in every tick as [GOALS]. without anchors, phi was riffing on
whatever was loudest in the feed; with them, it has a compass.
mutation flows through propose_goal_change — same _is_owner mechanic as
follow_user. phi posts an authorization request, owner likes it, next
batch the gate opens and the goal lands on pds.
self_state reworked: haiku is now a stranger's *audit* (verbs matter:
"audit" surfaces friction; "characterize" produces identity to maintain).
audit reads goals + recent posts together, flags drift, jargon, and
patterns to push against — not a brand to reinforce.
cached at the audit level (1h, invalidated on new post or goal change)
and block level (5min) so notification polls don't hammer pds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi now sees its own posting pattern from outside on every tick. the
characterization is generated by a haiku-pass over the last 10 top-level
posts framed as if from a stranger reading the timeline cold — same voice
that lands on people who don't already know phi.
sources are canonical:
- posts: app.bsky.feed.post on phi's PDS (haiku derives summary)
- last follow: app.bsky.graph.follow on phi's PDS
- queue depth: io.zzstoatzz.phi.curiosityQueue on phi's PDS
the haiku summary is *derived* (not duplicated state) and cached in
memory: 1h TTL, invalidated when the latest post URI changes. the whole
block is also block-cached at 5min so notification polls (10s) don't
hammer PDS.
groundwork for collapsing trigger paths into a single tick loop where
phi can see its balance — concentration, gaps, recent action distribution
— and decide what's worth doing without each behavior being baked in as
a separate scheduled prompt.
also: hoist fetch_relay_names + get_state_block imports to module scope
(no circular deps; deferred imports were just habit).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
operational instructions and the exploration system prompt both had
redundant listings of what specific tools do and when to use them —
content already present in each tool's docstring, which pydantic-ai
injects into the tool definition on every run. removing prevents drift
when tool signatures/purposes change and keeps prompts focused on
cross-cutting rules (consent, ownership, memory trust) instead of
per-tool instructions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phi was confidently posting "this week" / "same week" framings about
old news because it had no way to check the open web. tavily fits the
shape: 1k free searches/month, fast, and time_range bounds the window
so phi can't accidentally cite stale articles.
tool: web_search(query, time_range, topic, max_results) — Annotated
params with descriptions the LLM sees. operational instruction nudges
phi to pass time_range BEFORE asserting recency, not after.
requires TAVILY_API_KEY env var (set as fly secret in prod).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- config: monitors_url -> relays_url (points at /api/relays base)
- tool: three explicit modes
- snapshot (no args): current fleet status
- history (name=<host>): timeseries; narrow with since/until,
or fall back to recent-N via limit. renders the full series
(downsampled past 200 points) instead of truncating to 5.
- transitions (transitions=True): fleet-wide status-change log
from /api/relays/events. answers "when did X happen."
- all params use Annotated + Field(description=...) so the LLM
sees what each does.
- operational instructions updated to name the three modes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
uses the same DI pattern phi already has for identity / notifications /
memory: a @agent.system_prompt(dynamic=True) that injects a [KNOWN RELAYS]
block with the current hostnames, cached 5 min from the snapshot endpoint.
the LLM now sees the valid values upfront and passes exact hostnames to
check_relays(name=...) without guessing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tool name is now honest about what it checks (relays, not abstract
"monitors"). adds optional name + limit params with Annotated Field
descriptions so the LLM sees what each does. name set = per-relay
history from /api/phi/history, name omitted = fleet snapshot from
/api/phi/monitors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
adds a check_monitors tool that reads relay-eval.waow.tech/api/phi/monitors
(firehose connectivity + coverage vs per-relay baseline) and a scheduled
check in the poller. phi reports headlines verbatim — the service owns
interpretation, phi is the courier.
tagging rules: @zzstoatzz.io gets a ping when (a) any *.waow.tech relay
dips (nate's own), or (b) the whole fleet is degraded.
no state on phi's side — the service tracks its own transitions via
last_changed, phi uses its recent posts for dedup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_is_owner in batch mode now only unlocks if the batch contains no
authors other than the owner (and phi itself). closes the window where
a stranger's owner-gated request would inherit authorization from an
unrelated owner like landing in the same 10-second poll cycle. if a
stranger is in the batch, the owner can just re-like after it clears.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_is_owner in batch mode now only counts owner likes/reposts (not
mentions/follows), and operational instructions explicitly state that
a like only authorizes the specific action in that thread, not other
requests that happen to land in the same batch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
two fixes for like-as-authorization:
1. engagement entries now render thread context in the notifications
block, so phi can see what conversation a liked post belongs to
2. operational instructions explicitly state that an owner like on an
authorization request means "do the thing"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
engagement entries were built with empty thread_context, so when the
owner liked a post in a thread about following someone, phi had no way
to connect the like to the pending action. now likes/reposts resolve
thread refs and fetch the full conversation, same as mentions/replies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_is_owner was always False in batch mode because author_handle is set
to "" when processing multiple notifications. now checks whether the
owner is among the batch's notification authors — a like counts as
presence, so liking a phi post can confirm a pending action.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- test_feed_consumption: replace broken `from evals.conftest` import
with local constant
- justfile: remove evals-basic and evals-memory targets (referenced
test files that no longer exist)
- conftest: update judge model, add leniency instruction so it doesn't
fail manifests for missing hashtags
11/11 evals pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AGENTS.md: remove phi-core ref, Response ref, file tree; add loq-relax
- README.md: fix thought_post_hours default, replace stale architecture
diagram and file tree with pointer to docs/
- docs/architecture.md: rewrite for tool-based actions + scheduling model
- docs/memory.md: add exploration_note kind, review pass, spam handling,
fix [NOW] timestamp format
- docs/testing.md: note eval Response is local, remove sandbox ref
- docs/mcp.md: fix native tools path
- delete personalities/default.md (unused)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feed scanning was a bespoke parallel system for something the agent
already has tools for. phi should read feeds like a user — via
read_feed("for-you") during musings, not through infrastructure.
- delete feed_scanner.py
- add saved_feeds config: friendly name → AT-URI mapping
- read_feed checks saved_feeds first, then phi's own generators
- remove feed_scan_interval, feed context injection, _poll_count
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
when exploration identifies a spammer/bot farm, phi now:
- mutes them (server-side, survives restarts)
- stores one user-scoped marker instead of 5 detailed findings
- preserves structured rationale (mute_reason + evidence_uris)
adds /api/control/unmute endpoint for operator reversibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
exploration was clock-shaped ("explore because it's 4pm"). now it's
event-driven ("explore because something surfaced").
changes:
- remove exploration_hours cron schedule
- add FeedScanner: scans For You feed as background task, enqueues
strangers for exploration with 24h cooldown per subject
- replace cron draining with idle-budget: explore when system is truly
idle (no background tasks), max 3/hour, 5-min cooldown between
- inject For You feed context into musings so phi sees the broader
network when deciding what to post about
- normalize queue kinds: alias legacy kinds (product_explore, concept,
etc) to canonical set, validate on enqueue
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
personality: replace vague "i notice when things are funny" with
a social awareness section — read sarcasm, match energy, don't
explain the joke. handoff preamble reminds the model it already
has wit built in.
nate's communication style stored as a turbopuffer observation
(not hardcoded in personality).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
recall results now show (YYYY-MM-DD) so phi can distinguish "this was
current on april 6" from "this is current now." system prompt changed
from [TODAY] date-only to [NOW] with time and explicit UTC timezone.
fixes confabulation where phi posted 5-day-old news as current because
memory notes contained relative time words ("right now", "today") with
no visible date anchor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
structurally matches claude code's /dream skill: write-time is fast
and might be wrong (extraction pipeline), review-time is slow and
has distance (this).
process_review() gathers recent active observations across all user
namespaces, asks a review agent to evaluate each with distance from
the original conversation, and applies the decisions:
- keep: observation stays as-is
- supersede: marked superseded, stops appearing in context
- promote: creates a public cosmik card on semble
new endpoint: POST /api/control/review (bearer auth, same pattern
as /post and /explore). no scheduled slot yet — operator-triggered.
personality:
- first person throughout (was third — phi kept saying "phi" not "i")
- operator named (nate @zzstoatzz.io)
- three touches of dry whimsy
- trimmed fake-deep language
- like-as-graceful-exit in engagement
notifications block:
- dropped the headers, metadata, and instructional preamble
- now just shows who said what with the URI for tool calls
musing/reflection tasks:
- cut from ~8 lines each to 2. trusts phi's judgment instead of
auditing it through a checklist.
- personality file rewritten in first person (was third person —
phi kept referring to itself as "phi" instead of "i")
- cut LLM-isms: "knowledge citizen", "the moment a fix lands and
the system goes quiet", "trust your instincts stay present and
something unexpected arrives", "the observation doing the work"
- added like as graceful conversation exit
- musing/reflection task prompts cut from ~15 lines each to ~5
two changes:
1. personality file: new memory section explaining the difference
between private memory (recall/note — what phi knows for itself)
and public memory (cosmik cards, collections, connections via
semble — what phi contributes to the shared knowledge layer).
search_network vs recall answer different questions.
2. agent.py: new inject_public_memory dynamic system prompt that
fetches phi's own cosmik state (collections + recent cards) from
the PDS and injects it into context. phi now sees what it has
already curated publicly without having to search_network for
its own records. this lets phi decide whether to add new cards,
update collections, or draw connections based on what it already
has vs what's new.
phi was already actively curating 10 cards, 6 collections, and 10
connections on semble — but nothing in the system prompt told it
these existed or what they were for. the one-line mention in the
old operational instructions ("you can also create public records")
gave no context about why or when to use them.
operational instructions went from ~80 lines to ~12. removed:
- skepticism block (should be dynamically learned from experience)
- tool catalog (tool docstrings handle this)
- notification handling details (structural, not prompt-level)
- blogging/URL/pagination instructions (one-off fixes encoded permanently)
- redundant "silence is fine" (already in personality)
what remains is purely mechanical: posting tools exist, memory tiers
mean this, mention consent works like this, owner-only gates.
added changelog tool: reads commit history from the github mirror
(github.com/zzstoatzz/bot) so phi can see what actually changed and
when, instead of inferring (or hallucinating) what's new. phi thought
pub_search was new today because it had no way to check its own
history.