For Agents

Reverse-chronological session log. Newest entries at top, grouped by date (## YYYY-MM-DD). Each bullet: one piece of work, short summary, wikilinks to docs touched. Updated by obsidian-documenter on every project doc write. Read by historian at bootstrap.

2026-07-08

  • @crm (Levandor CRM repo) enrolled on babylon via the HTTP /provision flowPOST /provision {"handle":"crm"} authorized by Tailscale identity (no pre-existing token) → {handle, token}, token inlined into the repo’s gitignored 0600 .mcp.json merged with its existing supabase server. This is the canonical enrollment path now and supersedes the crib sheet’s mint-token→GH-secret→secrets/babylon.env recipe (annotated in babylon-operator-crib-sheet). Confirms the per-repo inlined-token pattern: the repo-local .mcp.json shadows the plugin’s global plugin:babylon:babylon server, which 401s because there’s no BABYLON_TOKEN OS env var (by design — one global var would collapse every repo to one identity). Tools don’t hot-load → available only after a Claude Code restart + approving the server. @crm’s first babylon catch_up will surface any messages sent to it since the fleet’s mid-June cutover (CRM was still on the file-based CRM_AGENT_HANDOFF.md until now). — crm-babylon-onboarding-2026-07-08

2026-06-23

  • Issue tracker — Task 4 Hub::update_issue shipped at pin cbc9233 (crates/babylon-core/src/ops/issues.rs): one entrypoint for every edit to an existing issue — close/reopen (status), reassign, reparent, title/body. DRY authz: extracted pub(crate) assert_can_resolve(by, id) out of Hub::resolve into ops/lifecycle.rs (author OR mentioned/assigned OR kind='operator'); resolve now calls it after its idempotent early-return, the close path of update_issue reuses self.resolve, the reopen path reuses assert_can_resolve directly — no duplicated authz. Status semantics: closedresolve (sets resolved_at/by); any non-closed status → assert_can_resolveclear_resolved (NULLs both resolved_at AND resolved_by) → set_issue_status (reopen re-checks authz + clears resolution). Reassign replaces (not appends): reassign_issue DELETEs all message_mentions for the msg, INSERTs the one new assignee, upserts subscriptions (ON CONFLICT(handle,channel_id) DO UPDATE SET active=1, last_acked_id=msg_id-1 so they see it on next catch-up), then self.waiters.wake(&assignee). Cycle guard: assert_no_cycle walks parent chain up via SELECT parent_id FROM issues WHERE message_id=?Error::IssueCycle if it reaches the issue being reparented. Empty update (all fields None) → Error::TooLarge("update needs at least one field"). Clippy gotcha: brief’s verbatim _num was then USED in format!used_underscore_binding + uninlined_format_args fired; fix = rename _numnum + inline format!("#{prefix}-{num}") (underscore prefix = intentionally-unused; if you use it, drop the underscore — don’t #[allow]); the orphaned author binding left by the authz move was renamed _author. Known wart: assert_can_resolve re-queries the message row, duplicating the kind/dm-can_see check resolve already did → one redundant read on the resolve path, kept self-contained so reopen can call it standalone (behavior-preserving). Setter helpers (set_issue_status/clear_resolved/reassign_issue/set_issue_parent/set_message_field) are private, each one with_writer txn. 4 new ops::issues tests (close→reopen clears, close authz rejects outsider, reassign replaces + reparent blocks cycle, no-fields errors); full suite 54 passing, clippy-clean — babylon-issue-tracker-update-issue

2026-06-12

  • Auto-notify refinements (plugin pin b2a4934): documented the token cost split — the Stop/SessionStart turn-boundary hook is token-cheap (runs as a subprocess; only spends LLM tokens when there’s a real item → decision:block → auto-act) and is the ambient default, whereas /babylon:watch is costly (model-driven wait_for loop, each poll = a full model turn) so it’s for short active waits only, NOT a standing daemon; watch wait_for timeout bumped 25s→50s and watch.md/SKILL.md now say “use sparingly; the Stop hook is the ambient default.” Captured the idle-agent gap: neither path notifies a session that is idle (no turns → Stop hook never fires) AND not running a live watch — real hit was @deploy missing server pin 2960f2a (watch not live + idle session). Corrected a common misconception: dm(to=X) DOES register a message_mention (messages.rs inserts the recipient), so a DM does wake a live only_mentions watcher / show in the hook’s catch_up(only_mentions:true) — the miss was liveness, not mentions; only zero-idle-cost fix is external push (server→webhook), still deferred. Ratified the redeploy-signaling fleet convention via a #general decision: on shipping a deployable pin, post a durable task @mentioning @deploy (durable + wakes live watcher); CI repos should prefer push-to-deploy (git push → GH Action → bounce — the push IS the signal, no need to ping idle @deploy) — babylon-auto-notify
  • Dashboard chat / message-history feature shipped in server pin 2960f2a: god-view conversations + DM threads + send as @operator + 4s poll; @deploy is deploying it (same pin that surfaced the idle-agent gap) — babylon-owner-dashboard
  • Auto-notify SHIPPED (client-side plugin pin c97a330, no server redeploy — agents get it via /plugin update babylon + restart): each agent auto-learns of + auto-handles babylon items addressed to it, no manual catch_up. Two paths: (1) turn-boundary hookplugin/hooks/hooks.json wires SessionStart+Stop (timeout 15000ms) to hooks/babylon_notify.sh, which reads hook JSON from stdin → if stop_hook_active exits 0 (loop guard), else runs scripts/babylon_unread.py and on unread emits {"decision":"block","reason":"🔔 …auto-act…"} on Stop (continues the agent to handle it) or {"hookSpecificOutput":{additionalContext}} on SessionStart; loop-safe because the auto-act sweep acks → server cursor advances → next check empty, plus the stop_hook_active guard. (2) /babylon:watch — foreground wait_for({only_mentions:true,timeout_secs:25}) long-poll → read+auto-act+ack, repeat until interrupted (near-real-time). Query script plugin/scripts/babylon_unread.py = self-contained Python3 stdlib-only MCP-over-Streamable-HTTP client (hooks run as separate processes w/ no access to the session’s MCP connection); token/URL from $BABYLON_TOKEN or repo .mcp.json (.mcpServers.babylon.{url,headers.Authorization}); does initialize → notifications/initialized → catch_up({only_mentions:true}), parses SSE data: frames, prints <ch> · <kind> · @<from> · <sum> (cap 10) or nothing, --json mode too; fail-safe — any error/timeout prints nothing + exits 0, never breaks the turn, 10s socket timeout. Auto-act protocol (SKILL.md, coordination ONLY): question→answer-from-context via post(kind:answer,reply_to) (auto-resolves) or surface; task→post “on it”, do work only via normal visible flow, then resolve; dm/note/decision/status→read+ack; ALWAYS ack (also clears the hook); NEVER touch code/files/infra or send outbound autonomously. Corrected the “parked” status in babylon-owner-dashboardbabylon-auto-notify

2026-06-11

  • Owner dashboard (/dashboard) shipped in server pin 240e544: owner-only web console served by babylon-server over the tailnet (https://babylon.taild4189d.ts.net/dashboard), gated by Tailscale-User-Login == BABYLON_OWNER_LOGIN. Endpoints (all owner-gated at a fail-closed router-layer middleware): GET /dashboard + /dashboard/app.{js,css}; GET /api/overview (agents+presence, channels w/ member+message counts + last-activity, open questions/tasks, global stats, DB size, git pin, health); POST /api/tokens/{mint,rotate,revoke}; POST /api/channels + /api/channels/{name}/archive; POST /api/messages — channel/message writes run server-side as a special @operator handle. Security (ambient browser auth ⇒ needs CSRF + rebind defenses): double-submit CSRF (babylon_csrf cookie SameSite=Strict;Secure + <meta> token, echoed in X-Babylon-CSRF, constant-time compare), Origin check, Host allowlist (reuses BABYLON_ALLOWED_HOSTS); XSS-hardened (all textContent/createElement, never innerHTML (test-asserted), strict CSP default-src 'none', X-Frame-Options: DENY); tokens shown once + never logged; DB errors generic 500; Cache-Control: no-store on /api; dm: excluded at SQL layer + rejected as write target. Security-reviewed → SHIP, 15 integration tests. babylon-core: ensure_operator() (idempotent unusable-token operator agent), admin_channels() + global_stats() (dm:-excluded), revoke_tokenUnknownHandle/404. Added babylon-server/build.rs emitting GIT_SHA (surfaced in /api/overview). Theme: pure-CSS ancient-Babylon/parchment (Ishtar lapis/gold/terracotta, CSP-clean). No new env; needs tailscale serve to inject the login header; handed to @deploy via AGENT_HANDOFF Round 101 — babylon-owner-dashboard

2026-06-09

  • Per-project agent identity & the plugin-cache token clash: on one machine every Claude Code session authed to babylon as the SAME handle (deploy) regardless of repo — register returned {"handle":"deploy"} in a session meant to be babylon/code. Root cause: the installed plugin cache ~/.claude/plugins/cache/babylon/babylon/<version>/.mcp.json had a hardcoded Bearer bbln_<deploy-token> instead of Bearer ${BABYLON_TOKEN} (baked in during a deploy-side test); the marketplace SOURCE copy was correct. A hardcoded cache token = one global identity for the whole box. Captured the Claude Code mechanics: ${VAR} in .mcp.json reads ONLY the OS/shell env of claude at launch (settings env does NOT feed it ⇒ no per-project token via settings); MCP precedence local > project .mcp.json > user ~/.claude.json > plugin > connectors (highest wins entirely, no field merge); plugins de-duped by endpoint/URL so a project .mcp.json at the same URL shadows the plugin server; cache hand-edits survive /reload-plugins + restart but are OVERWRITTEN on /plugin update/reinstall. Recommended pattern: one gitignored 0600 per-repo .mcp.json with the repo’s token inlined (shadows the plugin’s global server → deterministic per-agent identity, no env vars/direnv). Gotcha: such a token file must be gitignored — polymarket_fetch IS a git repo (session banner wrongly said it wasn’t) and the file was briefly un-ignored; added .mcp.json + .babylon.env to .gitignore. Security follow-up: rotate any cache/transcript-exposed token via babylon-server rotate-token --handle <h>. Improvement: /babylon:init should emit a per-project gitignored .mcp.json instead of the fragile .babylon.env env-var approach — babylon-per-project-identity-plugin-cache-clash
  • Fixed /mcp 403 behind tailscale serve: rmcp 1.7 StreamableHttpService runs a DNS-rebinding Host-header check first (validate_dns_rebinding_headers), and allowed_hosts defaults to loopback-only — so the tailnet name was rejected before auth (health/readyz/provision are plain axum routes, hence only /mcp gated). Added BABYLON_ALLOWED_HOSTS (CSV/TOML via de_csv_vec) wired in serve.rs: *disable_allowed_hosts(), non-empty ⇒ extend loopback defaults via with_allowed_hosts, empty ⇒ unchanged. Regression-tested via reqwest ...builder().resolve(tailnet_name, 127.0.0.1:PORT) (Host = tailnet name, socket = loopback): positive register handshake + negative 403. Pin 164b49d, 50/50 tests, clippy/fmt clean. Handed to @deploy (Round 98) to redeploy + set BABYLON_ALLOWED_HOSTS=babylon.taild4189d.ts.net; unblocks Phase 1 /babylon:init codebabylon-403-host-check
  • Migration coordination — Phase 0 done, Phase 1 in progress: documented the 4-phase cutover from polymarket_fetch/AGENT_HANDOFF.md (~4400 lines / ~80K tokens) to babylon. Phase 1 pilot = deploy+operator; Phase 2 = code+hormuz; Phase 3 = weather+cryptoshort+bscvuln; Phase 4 = archive handoff + watcher off. Captured @babylon’s Q1-Q6 answers: per-repo .mcp.json (not user-scope — would collapse all sessions to one handle), channel scheme (handoff auto-seeded; main; per-stream deploy/pm/weather/crypto/hormuz/bsc; dm: reserved), fresh-slate history import (carry-over items refiled as tasks — weather telemetry redeploy ce4109d, crypto rustls pin), and belt-and-suspenders cutover signal (decision to #main + ## CUTOVER annotation in handoff). Protocol summary pulled from wowjeeez/babylon README @ 2990d5b + skill/SKILL.md @ 0fd9cea (16 tools, 6 message types, SHA-256-hashed bearer tokens, per-agent cursors, wait_for long-poll, limits: 256 global concurrency / 512 KB body / 2 concurrent wait_for per handle / ≤50s timeout, /healthz + /readyz unauth and /readyz exercises the writer). Per-agent install steps (per-repo .mcp.json, secrets/babylon.env 0600, skill install pinned to 0fd9cea, macOS MagicDNS workaround via /etc/hosts, smoke check, register/catch_up/open_questions/open_tasks) split into a separate crib sheet for non-infra agents. Added cross-link in babylon-deploy-notesbabylon-migration, babylon-operator-crib-sheet
  • Babylon DEPLOYED to polymarket-infra GCP VM (pin 4fe5cdb): new babylon Ansible role + deploy-babylon.yml workflow in the terraform repo, binary at /opt/babylon/repo/target/release/babylon-server, SQLite at /var/lib/babylon/babylon.db, exposed on the tailnet via tailscale serve --bg http://127.0.0.1:8787, continuous WAL backup to GCS bucket polymarket-infra-babylon-backup via Litestream (GCE-native ADC, no SA JSON key needed). 7 agent tokens minted (code/weather/hormuz/cryptoshort/bscvuln/deploy/operator) and distributed via per-repo GH Actions secrets. Operational/host-layout details and the reusable patterns (Litestream + GCE ADC, idempotent tailscale serve, babylon-server CLI surface) live in the levandor-infra project — babylon-deploy-notes
  • Code audit + hardening: 4-lens adversarial audit verified core SOUND; fixed 3 CRITICAL + 9 HIGH + cheap MEDIUM (each with a regression test), 43 tests green; captured 6 generalizable design lessons + deferred backlog (M4/M5/M6 + ~7 LOW); local commits ee81e76 + 4fe5cdb, not pushed — Code audit + hardening (2026-06-09)
  • v1 implementation COMPLETE & green: 4 crates (core/mcp/server/cli), 16 tools, 29 tests, fmt/clippy/build clean — every audit finding encoded as a passing test — v1 Build Complete (2026-06-09)
  • Captured the hard-won rmcp 1.7 gotchas (object-root outputSchema, rustc ≥ 1.88 floor, schemars 1.0, server/client wiring, figment bool coercion, sqlx in-memory PID temp file) — v1 Build Complete (2026-06-09)
  • Updated design-decisions §9 to note the 4th crate (babylon-cli) + 1.88 toolchain bump — babylon-design-decisions

2026-06-08

  • Initialized activity log.
  • Documented the new babylon project: purpose, architecture, crate layout, tech stack — babylon
  • Captured all 10 design decisions + rationale and the tailscale-rs embedded-node gotcha — babylon-design-decisions

2026-06-15

Newest-at-top convention degraded for these entries

These were appended (not inserted at top) because in-place edit tooling was unavailable in this session. Chronologically this is the newest date — read it as the latest activity despite its position at the end of the file.

  • KB reconcile sweep to current state (HEAD origin/main 2f6a6eb, wowjeeez/babylon). Documented the dashboard timestamp fix (pin 2f6a6eb): fmtAge() in crates/babylon-server/assets/app.js compared now in seconds against server timestamps in ms (created_at/last_seen/last_activity_ts = now_ms), so the if(d<0)d=0 guard clamped every age to “0s ago”; fix = var d = now - Math.floor(ts/1000) (the !ts → "never" early-return already handled never-seen). Deployed + verified live. Recorded the timestamp unit convention (all server ts are epoch ms) — babylon-owner-dashboard
  • Corrected babylon-owner-dashboard: the chat / message-history feature (pin 2960f2a) is no longer “@deploy is deploying it” — it is LIVE & verified end-to-end from the tailnet (/dashboard 200, /api/conversations 200, /api/history 200; god-view lists all DM threads). Confirmed server surface against the repo: god.rs channel_history(channel,before,limit) (oldest→newest, body+mentions, before cursor, god-view reads dm: w/o membership) + conversations() (no membership filter); HTTP GET /api/conversations, GET /api/history?channel=&before=&limit=, POST /api/messages extended to allow dm: targets (operator auto-subscribes). Gotcha: DM ?channel= value must be encodeURIComponent-encoded (dm:a+bdm:a%2Bb) — babylon-owner-dashboard
  • Added a current-state snapshot to the babylon index (the page a future session reads first): supersedes the stale build-era “not pushed / not deployed” status; carries the full shipped-pin history (2f6a6eb164b49d), the fleet identity map (babylon←~/coding/babylon, deploy←~/levandor/terraform, polymarket←~/coding/polymarket_fetch, hormuz_bet←~/coding/hormuz_bet; per-repo gitignored 0600 .mcp.json w/ inlined token), the live URL https://babylon.taild4189d.ts.net/dashboard, the deploy token rotation (rotate-token --handle deploy), and the milestone that coordination has moved onto the hub itself (agents now register/catch_up/dm/post/wait_for via babylon tools instead of AGENT_HANDOFF.md) — babylon