|
| 1 | +--- |
| 2 | +name: vicoop-bridge-logs |
| 3 | +description: Inspect the runtime logs of a deployed vicoop-bridge server on Fly.io. Captures the server's stdout and summarizes its structured JSON event stream — agent requests, task completions/failures, rejections (by reason), client connect/disconnect, and backend activity — converting timestamps to KST. Complements admin-API metrics by exposing live runtime events. Use when the user says "check bridge logs", "브리지 로그 봐줘", "who got rejected / 누가 거부됐는지", "is backend X getting traffic / 백엔드 가용성", "task 완료·실패 추적", "client connect 추적", "bridge runtime logs / 브리지 런타임 로그". |
| 4 | +allowed-tools: Bash |
| 5 | +--- |
| 6 | + |
| 7 | +# vicoop-bridge-logs |
| 8 | + |
| 9 | +Read and summarize the **runtime logs** of a deployed `vicoop-bridge` **server** |
| 10 | +(`packages/server`, the gateway deployed via `fly deploy`). Every server log line |
| 11 | +is a single JSON object emitted by `logEvent()` |
| 12 | +(`packages/server/src/log.ts`) — `{ ...fields, event, ts }` — so the logs are a |
| 13 | +structured event stream, not free text. This skill captures a recent window with |
| 14 | +`fly logs` and summarizes it. |
| 15 | + |
| 16 | +> **Scope.** This is read-only log inspection of the **server** deployment. It does |
| 17 | +> not SSH, mutate the app, or touch the database. App lifecycle (status, restarts, |
| 18 | +> Postgres) is generic Fly.io work — use your Fly ops tooling for that. |
| 19 | +
|
| 20 | +## Install |
| 21 | + |
| 22 | +This repo ships the skill source under `skills/vicoop-bridge-logs/`. Drop it into |
| 23 | +your agent's skills tree on the host: |
| 24 | + |
| 25 | +**Claude Code** (user-wide): |
| 26 | + |
| 27 | +```bash |
| 28 | +DEST=~/.claude/skills/vicoop-bridge-logs |
| 29 | +mkdir -p "$DEST" |
| 30 | +cp -R skills/vicoop-bridge-logs/. "$DEST/" |
| 31 | +``` |
| 32 | + |
| 33 | +Project-scoped variant: replace `~/.claude` with `.claude` inside the repo you're |
| 34 | +working in. **Codex**: replace `~/.claude` with `~/.codex`. |
| 35 | + |
| 36 | +## Prerequisites |
| 37 | + |
| 38 | +- `flyctl` authenticated, with access to the org that owns your bridge server app. |
| 39 | +- `jq` for parsing the JSON event stream. |
| 40 | +- The **Fly app name** of your server deployment. Set it once per session; every |
| 41 | + snippet below honors it and falls back to the conventional `vicoop-bridge-server`: |
| 42 | + |
| 43 | + ```bash |
| 44 | + export BRIDGE_APP="${BRIDGE_APP:-vicoop-bridge-server}" |
| 45 | + ``` |
| 46 | + |
| 47 | + If you don't know it, `fly apps list` (filter for your bridge) or check the app's |
| 48 | + `fly.toml`. |
| 49 | + |
| 50 | +## Core capture pattern |
| 51 | + |
| 52 | +`fly logs --no-tail` prints the recent buffer then **blocks waiting for new lines** |
| 53 | +— it does not exit on its own. Bound it: |
| 54 | + |
| 55 | +```bash |
| 56 | +# Linux (coreutils `timeout` available): |
| 57 | +timeout 10 fly logs -a "$BRIDGE_APP" --no-tail 2>/dev/null |
| 58 | + |
| 59 | +# macOS / no `timeout` — run in background and kill after a few seconds: |
| 60 | +( fly logs -a "$BRIDGE_APP" --no-tail 2>/dev/null & p=$!; sleep 10; kill "$p" 2>/dev/null ) |
| 61 | +``` |
| 62 | + |
| 63 | +flyctl prints lines as `<ts> app[<machine>] <region> [info]{...json...}` with ANSI |
| 64 | +color. Extract the JSON payload with `grep -oE '\{.*\}'` (this also drops the ANSI |
| 65 | +noise) and parse with `jq`: |
| 66 | + |
| 67 | +```bash |
| 68 | +( fly logs -a "$BRIDGE_APP" --no-tail 2>/dev/null & p=$!; sleep 10; kill "$p" 2>/dev/null ) \ |
| 69 | + | grep -oE '\{.*\}' | jq -c 'select(.event)' |
| 70 | +``` |
| 71 | + |
| 72 | +The flyctl stderr line `Warning: Metrics token unavailable…` is harmless — that's |
| 73 | +why the snippets drop stderr (`2>/dev/null`). |
| 74 | + |
| 75 | +## Event reference |
| 76 | + |
| 77 | +Derived from `logEvent(` call sites in `packages/server/src`. Every event carries |
| 78 | +`event` and `ts`; the fields below are the notable extras. Optional fields are |
| 79 | +omitted when absent. |
| 80 | + |
| 81 | +### Request lifecycle |
| 82 | + |
| 83 | +| `event` | source | key fields | meaning | |
| 84 | +|--------------------------|-----------------------|------------|---------| |
| 85 | +| `agent_request` | `http.tsx` | `agentId`, `backend`, `hasAuth`, `ownerEmail?`, `principalId?`, `callerEmail?` | inbound `POST /agents/:id` accepted (the common "traffic" event) | |
| 86 | +| `agent_request_rejected` | `agent-auth.ts` | `agentId`, `reason`, `rejectionId`, `principalId?` | inbound request refused — **the key ops signal** (see reasons below) | |
| 87 | +| `task_completed` | `ws.ts` | `agentId`, `backend`, `taskId`, `contextId`, `state`, `principalId?` | client reported the task terminal; `state` is usually `completed` | |
| 88 | +| `task_failed_by_client` | `ws.ts` | `agentId`, `backend`, `taskId`, `contextId`, `errorCode`, `errorMessage`, `principalId?` | the connected agent failed the task | |
| 89 | +| `task_unreachable` | `executor.ts` | `agentId`, `taskId`, `contextId`, `principalId?` | no live client to deliver the task to (agent dropped mid-flight) | |
| 90 | + |
| 91 | +**`agent_request_rejected` reasons** (from `agent-auth.ts`, roughly in check order): |
| 92 | +`missing_bearer`, `bad_token_prefix`, `invalid_token`, `invalid_siwe_bearer`, |
| 93 | +`owner_session_on_caller_route`, `caller_not_authorized` (authenticated but not |
| 94 | +permitted for that agent), `agent_not_connected`, `agent_unavailable` (no backend |
| 95 | +registered for the agent id). The first six are auth failures; the last two mean |
| 96 | +the target agent isn't reachable. |
| 97 | + |
| 98 | +### Client / agent connection |
| 99 | + |
| 100 | +| `event` | source | key fields | meaning | |
| 101 | +|-----------------------|----------------|------------|---------| |
| 102 | +| `client_connected` | `ws.ts` | `agentId`, `clientId`, `ownerPrincipal`, `backend`, `email?` | an operator's agent attached over the outbound WebSocket | |
| 103 | +| `client_disconnected` | `ws.ts` | `agentId`, `clientId?`, `ownerPrincipal?`, `backend?`, `email?` | that agent detached | |
| 104 | +| `client_rejected` | `ws.ts` | `agentId`, `reason` | a WS handshake was refused (`agent id reserved`, `bad token`, `agent not allowed`, or a card/handshake-specific reason) | |
| 105 | +| `client_collision` | `registry.ts` | `agentId`, … | two clients claimed the same agent id | |
| 106 | + |
| 107 | +### Maintenance & internal errors |
| 108 | + |
| 109 | +| `event` | source | meaning | |
| 110 | +|----------------------------------|-------------------|---------| |
| 111 | +| `a2a_tasks_pruned` | `index.ts` | retention sweep deleted stale task contexts (`deleted`, `retentionDays`) | |
| 112 | +| `agent_lookup_failed` | `agent-auth.ts` | agent registry/db lookup failed for a request | |
| 113 | +| `task_persist_error` | `executor.ts` | best-effort task persistence failed (`taskId`, `error`) | |
| 114 | +| `admin_persist_error` | `admin.ts` | admin-side persistence failed | |
| 115 | +| `admin_api_error` | `http.tsx` | an admin API call errored | |
| 116 | +| `caller_last_used_touch_failed` | `auth/caller-token.ts` | best-effort `last_used_at` update failed (benign on read-only DB) | |
| 117 | +| `registry_agent_listener_error` | `registry.ts` | a registry listener threw | |
| 118 | + |
| 119 | +> This table is generated from the source; if you add a `logEvent(` call, add a row |
| 120 | +> here in the same change. Re-derive the full list with: |
| 121 | +> `grep -rhoE "logEvent\(['\"][a-zA-Z0-9_]+" packages/server/src | grep -oE "[a-zA-Z0-9_]+$" | sort -u` |
| 122 | +
|
| 123 | +**Privacy.** Events may carry `ownerEmail` / `callerEmail` / `email` and |
| 124 | +`principalId` / `ownerPrincipal` (namespaced `apikey:…`, `eth:0x…`, `google:…`). |
| 125 | +Caller-controlled strings are bounded via `truncate()` before logging, but treat a |
| 126 | +captured window as containing operator/user identifiers — don't paste it into |
| 127 | +public channels unredacted. |
| 128 | + |
| 129 | +## Subcommands |
| 130 | + |
| 131 | +Parse the subcommand from the request; default to `summary`. |
| 132 | + |
| 133 | +### summary (default) |
| 134 | + |
| 135 | +Tally events over a recent window and report health, KST-localized. |
| 136 | + |
| 137 | +```bash |
| 138 | +( fly logs -a "$BRIDGE_APP" --no-tail 2>/dev/null & p=$!; sleep 10; kill "$p" 2>/dev/null ) \ |
| 139 | + | grep -oE '\{.*\}' \ |
| 140 | + | jq -rc 'select(.event) | {event, reason, state, backend}' \ |
| 141 | + | sort | uniq -c | sort -rn |
| 142 | +``` |
| 143 | + |
| 144 | +Report: counts per `event` (per `reason` for rejections, per `state` for tasks), |
| 145 | +which backends are active, last activity time (max `ts`) **converted to KST |
| 146 | +(UTC+9)**, and any error-class events present. |
| 147 | + |
| 148 | +### rejections |
| 149 | + |
| 150 | +The most common reason to read these logs. |
| 151 | + |
| 152 | +```bash |
| 153 | +( fly logs -a "$BRIDGE_APP" --no-tail 2>/dev/null & p=$!; sleep 10; kill "$p" 2>/dev/null ) \ |
| 154 | + | grep -oE '\{.*\}' \ |
| 155 | + | jq -rc 'select(.event=="agent_request_rejected") | "\(.ts)\t\(.reason)\t\(.agentId)\t\(.principalId // "-")\t\(.rejectionId)"' |
| 156 | +``` |
| 157 | + |
| 158 | +Group by `reason` + `agentId`. `agent_unavailable` / `agent_not_connected` → the |
| 159 | +backend agent isn't attached (cross-check `client_connected` for that `agentId`). |
| 160 | +`caller_not_authorized` and the auth-failure reasons → report the offending |
| 161 | +`principalId`. |
| 162 | + |
| 163 | +### backend \<name\> |
| 164 | + |
| 165 | +Filter to one backend/agent (e.g. `claude`, a codex backend, `inline`). |
| 166 | + |
| 167 | +```bash |
| 168 | +( fly logs -a "$BRIDGE_APP" --no-tail 2>/dev/null & p=$!; sleep 10; kill "$p" 2>/dev/null ) \ |
| 169 | + | grep -oE '\{.*\}' \ |
| 170 | + | jq -rc --arg b NAME 'select(.event and (.backend==$b or ((.agentId // "")|test($b)))) | "\(.ts)\t\(.event)\t\(.agentId)\t\(.state // .reason // "-")"' |
| 171 | +``` |
| 172 | + |
| 173 | +Replace `NAME`. Answers "is backend X getting traffic / completing tasks / being |
| 174 | +rejected?". |
| 175 | + |
| 176 | +### tail [seconds] |
| 177 | + |
| 178 | +Bounded live follow (default 15s) — same kill-guard so it never blocks: |
| 179 | + |
| 180 | +```bash |
| 181 | +( fly logs -a "$BRIDGE_APP" 2>/dev/null & p=$!; sleep 15; kill "$p" 2>/dev/null ) |
| 182 | +``` |
| 183 | + |
| 184 | +(Without `--no-tail` it follows new lines.) Use when watching a deploy or |
| 185 | +reproducing a live rejection. |
| 186 | + |
| 187 | +### errors |
| 188 | + |
| 189 | +Surface non-routine activity: failures, rejections, and internal `*_error` events. |
| 190 | + |
| 191 | +```bash |
| 192 | +( fly logs -a "$BRIDGE_APP" --no-tail 2>/dev/null & p=$!; sleep 10; kill "$p" 2>/dev/null ) \ |
| 193 | + | grep -oE '\{.*\}' \ |
| 194 | + | jq -rc 'select(.event | test("rejected|failed|error|unreachable|collision")) | {ts, event, reason, errorCode, agentId}' |
| 195 | +``` |
| 196 | + |
| 197 | +## Tips |
| 198 | + |
| 199 | +- Always convert `ts` (ISO-8601 UTC) to **KST (UTC+9)** in summaries. |
| 200 | +- The capture only holds the recent buffer; `fly logs` has no start-time flag on |
| 201 | + the CLI. For historical lookups use the Fly dashboard log search. |
| 202 | +- Empty output usually means the machine is stopped or idle, not an error — confirm |
| 203 | + with `fly status -a "$BRIDGE_APP"`. |
| 204 | +- A recurring non-JSON `MaxListenersExceededWarning` line may appear; it's a Node |
| 205 | + runtime warning surfaced on stdout, not a per-request failure. The JSON-only |
| 206 | + snippets above (`grep -oE '\{.*\}'`) skip it. |
0 commit comments