Joel-approved cutover (2026-07-20): sessions.db is the only
full-transcript chunk index. The index-chunks Typesense import is
removed, ensureCollections no longer recreates run_chunks_dev, and the
stale web polling hint now points at joelclaw sessions search. Typesense
keeps runs_dev metadata for provenance and health.
convex/ (schema, functions, deploy role for :3210) moved to
joelclaw-api; apps/web consumes generated api/types via link:.
sessions schema is a deliberate temporary v.union (auth + call
telemetry shapes) — separation chartered. All four readback checks
pass: deploy clean, tsc clean, no old-path refs, live read works.
Worker-authored, steering-verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Built by a pi worker in a herdr pane (brief in .brain/data/tasks/), wired and
verified by the steering session end-to-end: public call → session + turn rows
in local Convex (tier-tagged, heartbeat liveness, 7-day purge) → hangup →
voice/public-call.completed → analyzer writes objective stats (judge: pending,
TODO pi-path rubric).
- infra/voice-agent/call_tracker.py: fire-and-forget Convex writes (1s timeout,
circuit breaker, caller hashing); wired into all four session tiers
- apps/live-dashboard: Vite+React reactive dashboard (active calls, latency,
analyses, transcript tails); schema deployed to local Convex :3210
- voice-public-call-analyze registered host-role; CONVEX_URL defaults localhost
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs-api can serve artifacts over SSH (DOCS_ARTIFACTS_SSH_*) when the
NAS mount is absent, docs CLI honors DOCS_API_URL/PDF_BRAIN_API_URL,
and Typesense searches respect TYPESENSE_SEARCH_CUTOFF_MS.
"This is known as graceful degradation." -- Secrets of the JavaScript
Ninja
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Panda is being decommissioned. The docs proxy's hardcoded fallback in
apps/web/app/api/docs/[[...path]]/route.ts pointed at panda's Tailscale
Funnel URL; it now falls back to flagg's docs-api Funnel URL instead.
DOCS_API_UPSTREAM_URL is already set in Vercel and points at flagg, so
this is behaviorally inert -- panda-decommissioning hygiene only.
"Deprecation can feel like the dirty work of cleaning up the street
after the circus parade has just passed through town, yet these
efforts improve the overall software ecosystem by reducing maintenance
overhead and cognitive burden of engineers." -- Software Engineering at
Google: Lessons Learned from Programming Over Time, Ch. 15 "Deprecation"
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the dev-bearer table with PDS-backed App Password authentication
per ADR-0243 Rule 20, scoped to single-user (Joel only) for now.
packages/system-bus/src/lib/pds.ts
+ pdsCreateAppPassword — calls com.atproto.server.createAppPassword on
the caller's session; returns { name, password, did, handle }
+ pdsRevokeAppPassword — revokes by name
+ pdsValidateAppPassword — one-shot validation via createSession; used
at register time only, NOT per-request (hash lookup is cheaper +
safer, and doesn't create server-side session state at PDS)
packages/memory/src/schemas/machines.ts — NEW Typesense schema for
machines_dev collection: id, user_id, did, handle, machine_name,
app_password_name, app_password_sha256, created_at, last_seen_at,
revoked_at
apps/web/lib/memory-auth.ts — NEW auth middleware
authenticateMemoryRequest(req) → MemoryIdentity | null
Hot path: sha256(bearer) → Typesense machines_dev lookup →
{ user_id, machine_id, did, source: "app-password" }
No PDS roundtrip per request. App Password validity is established
at register time; the hash is the identity key thereafter.
Dev-bearer fallback retained during transition (MEMORY_DEV_BEARER_TOKENS
env var); effectively disappears when that var is empty in prod.
Phase 3.5 TODO: users_dev collection (DID→user_id map is hardcoded for
now).
apps/web/app/api/runs/*/route.ts — all 6 handlers now call
authenticateMemoryRequest instead of the inline DEV_BEARER_TOKENS
table + local authenticate() helper. Rule 4 privacy filtering is
unchanged (still keyed on auth.user_id from the middleware).
scripts/joelclaw-machine-register.ts — NEW provisioning CLI
Flow: load cached PDS session from ~/.joelclaw/pds-session.json →
call com.atproto.server.createAppPassword → sha256(plaintext) →
upsert machines_dev row → back up old auth.json → write new
auth.json (0600) with the plaintext bearer. Ready to register Panda
once the infra is back.
Verification status (pending Typesense reachability):
[x] pds.ts compiles; curl-tested createAppPassword directly against
PDS (returned valid password)
[x] Bun fetch works against PDS directly (ruled out fetch-lib bugs)
[x] All route handlers typecheck + biome clean
[x] Auth middleware compiles
[ ] End-to-end: register panda → POST /api/runs with new bearer →
Run indexes → search returns it — BLOCKED on Colima SSH tunnel
which is flapping (ssh 192.168.64.2:22 timing out; matches the
2026-04-17 healer-suicide-loop memory note)
When infra comes back:
TYPESENSE_API_KEY=<...> joelclaw-machine-register --name panda --user joel
then POST /api/runs with the new bearer to confirm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inspired by pi's /tree (badlogic/pi-mono). Pi's tree is per-turn within
one session; this one is per-Run across all sessions and all runtimes.
Every Run carries parent_run_id + root_run_id (Rule 3), so the forest
exists in the schema — we just didn't have a view.
apps/web/app/api/runs/forest/route.ts
GET /api/runs/forest
?since=<epoch_ms> default: 7d window
?runtime=pi,claude-code comma-separated filter
?root_run_id=<id> zoom into a subtree
?limit=<n> cap 1-250 (Typesense per_page max)
Returns a flat list of Runs sorted by started_at + metadata for
client-side tree assembly. Privacy-filtered by readable_by (Rule 4).
scripts/joelclaw-runs-tree.ts → ~/.bun/bin/joelclaw-runs-tree
Terminal renderer with ANSI-colored runtime badges, box-drawing tree
connectors, relative timestamps, turn counts, tag chips, intent
clipped to 80 chars, clean sort order (started_at asc per bucket).
Orphan-root handling: a Run whose parent_run_id isn't in the result
set (trimmed by limit/since/runtime filters) becomes a visible root
rather than being dropped. Deterministic output.
Verified live:
$ joelclaw-runs-tree --since=6h --limit=100
3 Runs (3 roots) since 2026-04-19T20:09
├─ claude-code 22m ago 2t #phase1-v3
│ third attempt with typesense key in env
├─ claude-code 21m ago 1t #smoke-e2e
│ test
└─ claude-code 18m ago 82t #smoke-e2e
Explore the project at /Users/joel/Code/joelhooks/joelclaw…
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the three GET traversal endpoints from ADR-0243 Rule 13 (D shape):
apps/web/app/api/runs/[id]/route.ts
- GET: fetch the Run metadata row from runs_dev
- Enforces Rule 4 at read time by checking readable_by.includes(caller)
- 404 (not 403) on unauthorized — leaks zero information about existence
apps/web/app/api/runs/[id]/jsonl/route.ts
- GET: stream the full jsonl transcript from NAS
- Verifies readable_by on the Run row before streaming
- 410 Gone if the blob is missing on disk (distinct from 404 "no Run")
- Returns application/x-ndjson with Content-Length; zero buffering
apps/web/app/api/runs/[id]/descendants/route.ts
- GET: walk a Run's subtree via root_run_id filter
- Returns root + all descendants ordered by started_at ascending
- Lets agents reconstruct a workload-rig DAG or a gateway session tree
in one call
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the Phase 1 ingest + retrieval loop per ADR-0243 build order step 7.
apps/web
- new app/api/runs/search/route.ts — hybrid Typesense search with the
full agent-first API surface (Rule 12 + 13):
* Auto-applied privacy filters (user_id + readable_by) from the
bearer token — NEVER from the request body (Rule 4)
* Tag filters default to AND semantics
* Modes: hybrid (default), semantic, keyword
* Embeds the query at priority="query" so it preempts any pending
ingest work in the Ollama queue (Rule 9a)
* HATEOAS envelope with _links, next_actions, and timing breakdown
(query_embed_ms, query_queued_ms, typesense_ms, total_ms)
scripts/memory-spike
- new smoke-e2e.ts — full pipeline validation:
1. POST /api/runs with a real claude-code jsonl fixture
2. Poll Typesense for chunks matching the new run_id
3. POST /api/runs/search filtered to the smoke-e2e tag
4. Assert the ingested Run is findable
Run against a live dev stack (Next.js on :3000, worker deployed,
Typesense + Ollama + Inngest healthy).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the central ingest path for agent Run capture per ADR-0243 build order
steps 5 and 6:
packages/memory
- src/schemas/runs.ts — Typesense `runs_dev` collection schema covering
the full Run row from types.ts
- src/nas.ts — blob writer honoring Rule 11 (user-partitioned paths);
env var MEMORY_RUN_STORE defaults to ~/.joelclaw/runs-dev/ for local
development, set to /nas/memory/runs in production
packages/system-bus
- new memory/run.captured + memory/run.indexed event contracts
- new functions/memory/run-captured.ts — receives event, loads jsonl
from NAS (or inline payload), format-detects, chunks via per-turn
chunker, embeds each chunk at ingest-realtime priority through
@joelclaw/inference-router (so queries preempt), writes chunks to
run_chunks_dev and Run row to runs_dev, emits OTEL + fanout indexed
event; concurrency:4, retries:3; ensures collections exist idempotently
- @joelclaw/memory added to dependencies
apps/web
- new app/api/runs/route.ts — POST handler; dev bearer token auth
(hardcoded allowlist, PDS flow lands Phase 3), writes jsonl to NAS
via @joelclaw/memory writeRunBlob, fires memory/run.captured via
HTTP to local Inngest, returns 202 HATEOAS envelope with run_id +
_links + next_actions
- @joelclaw/inference-router + @joelclaw/memory added to dependencies
Still v1 / dev scope:
- Dev bearer token, not PDS App Password (Phase 3)
- _dev collection suffix, will alias to run_chunks_current after cutover
- No search endpoint yet (next slice)
- No capture hooks yet (Phase 4)
- No entity enrichment yet (Phase 6)
Typecheck clean, biome clean, 12 chunking tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fetchDocSummaries wrapped in try/catch — search still works if Typesense is slow.
handleStatus wrapped in try/catch — returns zeros if Typesense unreachable.
Global unhandledRejection handler prevents silent Bun crashes.
docs-api and CLI both tried vector_query with empty [] which only works with
Typesense auto-embed. v2 uses pre-computed ollama vectors. Text search on
retrieval_text still works. TODO: embed query via ollama for vector search.
- Created tutorial:ai-job-scheduling-macos-launchd
- Removed discovery:ai-job-scheduling-mac-local-first-video
- Added permanent redirect from /cool/ai-job-scheduling-mac-local-first-video to /ai-job-scheduling-macos-launchd