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