commits
the bot now records provenance (source_uris: list[AtUri]) on every
observation row — sources are what the claim is anchored to. the likes
pipeline has the sources for free (the URIs of the posts nate liked that
fed the extraction), but was throwing them away.
changes:
- load_recent_liked_posts SQL now selects subject_uri alongside the
other columns
- LikesObservation gets a source_uris field (default empty). the LLM
doesn't see URIs in its prompt (only post text), so the orchestrator
attaches them post-extraction: every observation for an author gets
the full deduped ordered set of URIs from that author's batch of
liked posts — coarse but always-true
- USER_NAMESPACE_SCHEMA aligned with the bot: added status, supersedes,
source_uris columns that previously diverged
- write_likes_observations_to_turbopuffer writes status="active",
supersedes="", source_uris=[...] so bot reads (which filter on
status != superseded) find them
bot side (separate commit) reads source_uris defensively via
getattr(row, "source_uris", []) so older rows missing the column don't
break anything.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
leaflet-search Pages project moved with the waow.tech zone migration to
N8@zzstoatzz.io. previous account ID was tied to the prefect.io SSO
account that's being phased out — flow deploys would fail next run.
new generic endpoint exposing authors of operator-liked posts (last N days,
default 7) with sample posts. consumers (any agent) can use this as a
high-signal pool of attention; per-consumer filtering happens client-side.
- web/src/lib/server/discovery.ts: loadDiscoveryPool() queries raw_liked_posts,
groups by author, returns top-N with sample posts (default top 30, 3 samples each)
- web/src/routes/api/agents/discovery-pool/+server.ts: GET endpoint with
optional ?window_days, ?max_authors, ?samples_per_author params
- web/src/lib/types.ts: DiscoveryPoolEntry / DiscoveryPoolPost types
not phi-specific by design — any consumer agent can use it. coupling kept
at the JSON schema, not at duckdb columns or per-consumer filtering logic.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Opt into the experimental V2 UI bundled in prefect >=3.6.9. The V2
assets are already packaged in prefecthq/prefect:3.6.23-python3.11
(the image we're running) — this setting just switches the server's
static-file serving from the V1 bundle to the V2 bundle.
Reference: prefecthq/prefect#20065 (Alex Streed, merged Dec 31 2025) —
added V2 UI packaging to the Dockerfile and the server.ui.v2_enabled
setting that chooses which bundle FastAPI mounts. When v2_enabled is
true, create_ui_app uses prefect.__ui_v2_static_path__ and logs
"Serving experimental V2 UI" at startup.
- records: list[dict] replaces record: dict (batch create support)
- updates: dict replaces overloaded record field for update action
- position ordering and [action]-scoped descriptions on all fields
- action renders as dropdown, fields are logically grouped
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
all 16 connections successfully recreated with correct schema
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
maps invalid types (illustrates→EXPLAINER, example-of→EXPLAINER, builds-on→SUPPORTS),
adds createdAt/updatedAt, preserves all original notes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- new flow parameterized by action (list/delete/create/update), collection, and filters
- uses pdsx for auth and operations instead of hand-rolled httpx
- validate connectionType in curate agent against allowed enum
- add pdsx dep to mps workspace package
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
semble requires these fields for indexing. without them, connections
exist in PDS but don't render in the UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
existing namespaces don't have this attribute yet — requesting it
causes a schema error. updated_at will appear on new writes; the
query can add it back once data has migrated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
step 1: compact.py gets updated_at in schema and likes writes.
step 2: curate.py gains observation-specific tools (list_users,
list_user_observations, deprecate_observation, update_observation)
and a second agent phase that reviews private observations for
contradictions, staleness, and near-duplicates.
also fixes ruff errors: ambiguous var name, unused exception var,
unused logger import.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
separate curate-model variable (default claude-haiku-4-5) so curation
doesn't share the morning-model config or hit sonnet's 30k/min input
token limit during multi-turn tool calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OpenAI/TurboPuffer client objects contain thread locks that can't be
serialized by Prefect's default cache policy. Set NONE explicitly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
morning.py Phase 4 was a single-shot LLM call that reconstructed phi's
context and made curation decisions as a different brain. result: 17
duplicate collections hourly, third-person notes, zero connections.
curate.py is an agentic loop with phi's personality and memory — it can
list, inspect, recall, and iterate. notes are written in first person.
triggered by morning flow completion via event bus.
- new flows/curate.py with pydantic-ai agent + curation tools
- prefect.yaml: curate deployment triggered on morning completion
- morning.py: Phase 4 removed (1145 -> 564 lines)
- deleted 59 collectionLinks + 17 duplicate collections from PDS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
adds compact, morning, rebuild-atlas flows. documents bluesky likes
and phi memory data sources. updates dbt model table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
resolve liked post content at ingest time (batch getPosts), extract
per-author observations in compact via LLM with structured output
(ADD/UPDATE/NOOP reconciliation), write as kind="observation" to
TurboPuffer user namespaces. observations flow through existing
morning flow, compact summaries, and phi runtime unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
og-image.js imports workers-og which needs to be installed from
site/package.json before wrangler can bundle the functions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrangler calls /memberships to discover the account, which requires
User:Memberships:Read scope. Setting CLOUDFLARE_ACCOUNT_ID skips that.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The site has Pages Functions (functions/ dir) that wrangler compiles into
a _worker.bundle. The raw Direct Upload API doesn't bundle functions,
so deploying without wrangler causes 500 errors on the live site.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
httpx data= sends url-encoded, but CF Pages expects multipart/form-data.
Use files= with (None, value) tuples to send proper -F style fields.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CF Pages Direct Upload API expects multipart form fields.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
builds the 2D semantic map (UMAP + HDBSCAN on tpuf vectors) and
deploys to Cloudflare Pages via Direct Upload API.
secrets: tpuf-token, cloudflare-api-token
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
weave's mechanical promotion (threshold-based cards/connections/collections)
produced 4 cards and 0 connections. replace with daily morning flow that
assembles both knowledge layers (tpuf + semble state), lets an LLM decide
what deserves promotion, then executes the plan atomically.
phases 1-3 (tag maintenance) unchanged. phase 4 is now:
assemble_curation_context → plan_curation → execute_curation_plan
schedule: 0 13 * * * (8am CT), 1h before phi's daily reflection.
NOT triggered by transform — runs on its own cron.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
existing card URLs were skipped in evidence collection, so they never
got mapped to tags. now all URLs are tracked in url_evidence (including
existing ones), and existing cards get mapped to their observation tags.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
the embedding-based _match_cards_to_tags returned 0 matches because short
URL strings don't embed close to tag names. now create_cluster_cards
returns both cards and a tag->card mapping built from the observations
that contained those URLs — direct association, no similarity threshold.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add create_cluster_cards task that scans all tpuf namespaces for URLs
in observations and creates cosmik URL cards before promotion
- fix _match_cards_to_tags to read nested content.metadata format
- solves chicken-and-egg: promotion needs cards, but cards didn't exist
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
instead of asking the LLM to evaluate pairs (which sonnet found zero of),
ask it to identify thematic clusters of tags, then derive pairwise edges
from cluster membership. this matches how the tags actually relate — they
form thematic groups, not isolated pairs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
reads `weave-model` variable at runtime (default: claude-sonnet-4-6).
changeable via `prefect variable set weave-model <model>` without redeploy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
give the LLM the full tag inventory with context so it can propose merges
and relationships directly, rather than only seeing pre-filtered pairs that
passed an embedding similarity threshold. the previous approach found zero
merges because short tag strings don't cluster well by cosine similarity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ByTagsHash now includes task_key in the cache key so identify_tag_merges
and discover_tag_relationships don't share cached results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
new flow triggered on transform completion (parallel with compact and brief).
reads tags from all tpuf namespaces, deduplicates via embedding similarity +
LLM confirmation, discovers tag relationships, stores edges in phi-tag-relationships
namespace, and selectively promotes high-confidence relationships to cosmik records.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
new data source: nate's bluesky likes fetched from PDS via
com.atproto.repo.listRecords. fetched in parallel with tangled/phi,
persisted sequentially to raw_likes table. dbt staging model deduplicates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
the multiplicative formula (recency * engagement * ...) gave score=0
to any item with no comments/reactions, burying 60% of open issues
including well-filed bugs. switch to additive blend where recency is
the primary signal and engagement boosts rather than gates.
140 → 14 zero-score items on current data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
the compaction LLM was guessing user names from context — it hallucinated
"zoë" for a user named nate. now fetches display name and bio from the
public bluesky API and includes them in the synthesis prompt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
when observations haven't changed, synthesize_summary returns a cached
result — but write_summary_to_turbopuffer was still making an OpenAI
embedding call + TurboPuffer upsert with identical data every cycle.
add BySummaryContent cache policy to the write task, keyed on
handle + summary text hash. now both the LLM call and the write are
skipped when nothing has changed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dbt staging models are materialized as views whose definitions reference
a catalog path that doesn't survive the DuckDB snapshot copy. query raw
tables directly with inline dedup instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ingest flow now fetches observations + interactions from phi's TurboPuffer
namespaces alongside github/tangled data. new dbt models stage and profile
per-user data. new compact flow (triggers on transform, parallel with brief)
synthesizes relationship summaries via haiku and writes them back to
TurboPuffer where phi reads them at conversation time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ingest → transform → brief reads as a clear pipeline.
the old names sounded like synonyms.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
both sources fetched concurrently in one flow, DuckDB writes sequential
in the same process — eliminates the staggered cron hack. enrich now
triggers on ingest completion instead of tangled-items.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- header: semantic HTML with nav links to prefect server and source
- filter bar: full-width search on mobile, flex selects, aria labels
- card table: mobile card view (MobileCard.svelte) with sort control,
aria-sort on desktop table headers
- stat bar: tabular-nums, role="group"
- card row: better contrast on timestamps
- app.css: global focus-visible outline, prefers-reduced-motion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
replace cron schedules on enrich and curate with event-driven
triggers. enrich fires when tangled-items completes (both data
sources are in DuckDB by then). curate fires when enrich completes.
no pods spin up unless upstream actually finished.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ByItemsContent cache policy hashes the scored items text — when the
top-200 list hasn't changed, the briefing is served from cache and
haiku is never called. 4h expiration. drops the timestamp from the
LLM prompt so input is purely content-dependent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add MIT license
- rewrite README to lead with the pipeline, not the infrastructure
- remove unused prefect/ terraform directory (superseded by scripts/patch_work_pool.py)
- note ANTHROPIC_API_KEY in .env.example (it's a prefect secret block)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
prefect.runtime.flow_run.parameters returns validated pydantic objects
(not raw dicts) when FlowRunContext is active. RetentionConfig has no
.get() method — use getattr instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cleanup: shows dry-run/live mode and retention window (e.g. "dry-run-30d")
gh-notifications: shows "unread" or "all"
diagnostics: add explicit name="diagnostics" for consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
matches card meta.user against known handles (zzstoatzz, zzstoatzz.io).
small sky-colored label between the issue number and note text.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- curate prompt: exactly 4 sections, 4-6 items each, no highlights,
all normal priority
- frontend: cap sections at 4, remove orphan/highlight/colSpan logic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tested: two separate processes writing to the same DuckDB file fails
with a lock error. DuckDB only supports one writer process at a time.
gh-notifications and tangled-items must not overlap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DuckDB supports concurrent writers through WAL — the contention was
from Grafana's exclusive lock, not between the two ingestion flows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
the bot now records provenance (source_uris: list[AtUri]) on every
observation row — sources are what the claim is anchored to. the likes
pipeline has the sources for free (the URIs of the posts nate liked that
fed the extraction), but was throwing them away.
changes:
- load_recent_liked_posts SQL now selects subject_uri alongside the
other columns
- LikesObservation gets a source_uris field (default empty). the LLM
doesn't see URIs in its prompt (only post text), so the orchestrator
attaches them post-extraction: every observation for an author gets
the full deduped ordered set of URIs from that author's batch of
liked posts — coarse but always-true
- USER_NAMESPACE_SCHEMA aligned with the bot: added status, supersedes,
source_uris columns that previously diverged
- write_likes_observations_to_turbopuffer writes status="active",
supersedes="", source_uris=[...] so bot reads (which filter on
status != superseded) find them
bot side (separate commit) reads source_uris defensively via
getattr(row, "source_uris", []) so older rows missing the column don't
break anything.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
new generic endpoint exposing authors of operator-liked posts (last N days,
default 7) with sample posts. consumers (any agent) can use this as a
high-signal pool of attention; per-consumer filtering happens client-side.
- web/src/lib/server/discovery.ts: loadDiscoveryPool() queries raw_liked_posts,
groups by author, returns top-N with sample posts (default top 30, 3 samples each)
- web/src/routes/api/agents/discovery-pool/+server.ts: GET endpoint with
optional ?window_days, ?max_authors, ?samples_per_author params
- web/src/lib/types.ts: DiscoveryPoolEntry / DiscoveryPoolPost types
not phi-specific by design — any consumer agent can use it. coupling kept
at the JSON schema, not at duckdb columns or per-consumer filtering logic.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Opt into the experimental V2 UI bundled in prefect >=3.6.9. The V2
assets are already packaged in prefecthq/prefect:3.6.23-python3.11
(the image we're running) — this setting just switches the server's
static-file serving from the V1 bundle to the V2 bundle.
Reference: prefecthq/prefect#20065 (Alex Streed, merged Dec 31 2025) —
added V2 UI packaging to the Dockerfile and the server.ui.v2_enabled
setting that chooses which bundle FastAPI mounts. When v2_enabled is
true, create_ui_app uses prefect.__ui_v2_static_path__ and logs
"Serving experimental V2 UI" at startup.
- records: list[dict] replaces record: dict (batch create support)
- updates: dict replaces overloaded record field for update action
- position ordering and [action]-scoped descriptions on all fields
- action renders as dropdown, fields are logically grouped
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- new flow parameterized by action (list/delete/create/update), collection, and filters
- uses pdsx for auth and operations instead of hand-rolled httpx
- validate connectionType in curate agent against allowed enum
- add pdsx dep to mps workspace package
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
step 1: compact.py gets updated_at in schema and likes writes.
step 2: curate.py gains observation-specific tools (list_users,
list_user_observations, deprecate_observation, update_observation)
and a second agent phase that reviews private observations for
contradictions, staleness, and near-duplicates.
also fixes ruff errors: ambiguous var name, unused exception var,
unused logger import.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
morning.py Phase 4 was a single-shot LLM call that reconstructed phi's
context and made curation decisions as a different brain. result: 17
duplicate collections hourly, third-person notes, zero connections.
curate.py is an agentic loop with phi's personality and memory — it can
list, inspect, recall, and iterate. notes are written in first person.
triggered by morning flow completion via event bus.
- new flows/curate.py with pydantic-ai agent + curation tools
- prefect.yaml: curate deployment triggered on morning completion
- morning.py: Phase 4 removed (1145 -> 564 lines)
- deleted 59 collectionLinks + 17 duplicate collections from PDS
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
resolve liked post content at ingest time (batch getPosts), extract
per-author observations in compact via LLM with structured output
(ADD/UPDATE/NOOP reconciliation), write as kind="observation" to
TurboPuffer user namespaces. observations flow through existing
morning flow, compact summaries, and phi runtime unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
weave's mechanical promotion (threshold-based cards/connections/collections)
produced 4 cards and 0 connections. replace with daily morning flow that
assembles both knowledge layers (tpuf + semble state), lets an LLM decide
what deserves promotion, then executes the plan atomically.
phases 1-3 (tag maintenance) unchanged. phase 4 is now:
assemble_curation_context → plan_curation → execute_curation_plan
schedule: 0 13 * * * (8am CT), 1h before phi's daily reflection.
NOT triggered by transform — runs on its own cron.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
the embedding-based _match_cards_to_tags returned 0 matches because short
URL strings don't embed close to tag names. now create_cluster_cards
returns both cards and a tag->card mapping built from the observations
that contained those URLs — direct association, no similarity threshold.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add create_cluster_cards task that scans all tpuf namespaces for URLs
in observations and creates cosmik URL cards before promotion
- fix _match_cards_to_tags to read nested content.metadata format
- solves chicken-and-egg: promotion needs cards, but cards didn't exist
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
instead of asking the LLM to evaluate pairs (which sonnet found zero of),
ask it to identify thematic clusters of tags, then derive pairwise edges
from cluster membership. this matches how the tags actually relate — they
form thematic groups, not isolated pairs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
give the LLM the full tag inventory with context so it can propose merges
and relationships directly, rather than only seeing pre-filtered pairs that
passed an embedding similarity threshold. the previous approach found zero
merges because short tag strings don't cluster well by cosine similarity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
new flow triggered on transform completion (parallel with compact and brief).
reads tags from all tpuf namespaces, deduplicates via embedding similarity +
LLM confirmation, discovers tag relationships, stores edges in phi-tag-relationships
namespace, and selectively promotes high-confidence relationships to cosmik records.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
the multiplicative formula (recency * engagement * ...) gave score=0
to any item with no comments/reactions, burying 60% of open issues
including well-filed bugs. switch to additive blend where recency is
the primary signal and engagement boosts rather than gates.
140 → 14 zero-score items on current data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
when observations haven't changed, synthesize_summary returns a cached
result — but write_summary_to_turbopuffer was still making an OpenAI
embedding call + TurboPuffer upsert with identical data every cycle.
add BySummaryContent cache policy to the write task, keyed on
handle + summary text hash. now both the LLM call and the write are
skipped when nothing has changed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ingest flow now fetches observations + interactions from phi's TurboPuffer
namespaces alongside github/tangled data. new dbt models stage and profile
per-user data. new compact flow (triggers on transform, parallel with brief)
synthesizes relationship summaries via haiku and writes them back to
TurboPuffer where phi reads them at conversation time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- header: semantic HTML with nav links to prefect server and source
- filter bar: full-width search on mobile, flex selects, aria labels
- card table: mobile card view (MobileCard.svelte) with sort control,
aria-sort on desktop table headers
- stat bar: tabular-nums, role="group"
- card row: better contrast on timestamps
- app.css: global focus-visible outline, prefers-reduced-motion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ByItemsContent cache policy hashes the scored items text — when the
top-200 list hasn't changed, the briefing is served from cache and
haiku is never called. 4h expiration. drops the timestamp from the
LLM prompt so input is purely content-dependent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>