# vllmstat v0.4.0 — Tee Phase 1 (log request feed) — Implementation Plan

> REQUIRED SUB-SKILL: superpowers:subagent-driven-development. TDD, checkbox steps.
> Spec: `docs/superpowers/specs/2026-06-15-tee-design.md`.

**Goal:** A live **TEE** panel under the detail dashboard showing a feed of HTTP requests parsed from
the vLLM server's logs (Docker container or file). No content (modern vLLM doesn't log it) — method,
path, status, client. `t` toggles the panel; `--logs SOURCE` / config `logs=` / `--discover-docker`
provide the source.

**Conventions:** Python ≥3.10, `from __future__ import annotations`, dataclasses, ruff+pyright clean,
pytest (`source .venv/bin/activate`). Commit after each task. The event model is shared with Phase 2
(proxy), so keep `TeeEvent` general.

---

## Task 1: `TeeEvent` + `TeeBuffer`

**Files:** Create `src/vllmstat/core/tee.py`; Test `tests/test_tee.py`.

- [ ] Tests:
```python
def test_tee_event_defaults():
    from vllmstat.core.tee import TeeEvent
    e = TeeEvent(ts=1.0, kind="http", method="POST", path="/v1/chat/completions", status=200)
    assert e.prompt is None and e.done is True and e.client is None

def test_tee_buffer_recent_and_maxlen():
    from vllmstat.core.tee import TeeBuffer, TeeEvent
    b = TeeBuffer(maxlen=3)
    for i in range(5):
        b.push(TeeEvent(ts=float(i), kind="note", text=str(i)))
    assert len(b) == 3
    assert [e.text for e in b.recent(2)] == ["3", "4"]   # newest last
    assert b.recent(0) == []
```
- [ ] Implement `core/tee.py`:
```python
from __future__ import annotations
from collections import deque
from dataclasses import dataclass


@dataclass
class TeeEvent:
    ts: float
    kind: str  # "http" | "exchange" | "note"
    method: str | None = None
    path: str | None = None
    status: int | None = None
    client: str | None = None
    endpoint: str | None = None
    prompt: str | None = None
    response: str | None = None
    prompt_tokens: int | None = None
    completion_tokens: int | None = None
    streaming: bool = False
    done: bool = True
    text: str | None = None


class TeeBuffer:
    def __init__(self, maxlen: int = 500) -> None:
        self._events: deque[TeeEvent] = deque(maxlen=maxlen)

    def push(self, event: TeeEvent) -> None:
        self._events.append(event)

    def recent(self, n: int) -> list[TeeEvent]:
        return list(self._events)[-n:] if n > 0 else []

    def __len__(self) -> int:
        return len(self._events)
```
- [ ] Commit: `feat(tee): TeeEvent + TeeBuffer`.

---

## Task 2: log access-line parser + `LogTailer`

**Files:** Create `src/vllmstat/providers/logsource.py`; Test `tests/test_logsource.py`.

- [ ] Tests (parser uses the REAL captured format):
```python
import asyncio
from vllmstat.providers.logsource import parse_access_line, LogTailer
from vllmstat.core.tee import TeeEvent

LINE = '(APIServer pid=1) INFO:     172.18.0.1:47402 - "POST /v1/chat/completions HTTP/1.1" 200 OK'

def test_parse_access_line_real():
    e = parse_access_line(LINE, now=1.0)
    assert e and e.kind == "http" and e.method == "POST"
    assert e.path == "/v1/chat/completions" and e.status == 200 and e.client == "172.18.0.1"

def test_parse_filters_health_and_metrics():
    assert parse_access_line('INFO: 1.2.3.4:5 - "GET /health HTTP/1.1" 200 OK', now=1.0) is None
    assert parse_access_line('x - "GET /metrics HTTP/1.1" 200 OK', now=1.0) is None

def test_parse_4xx_and_nonaccess():
    e = parse_access_line('h 9.9.9.9:1 - "POST /v1/completions HTTP/1.1" 400 Bad Request', now=1.0)
    assert e and e.status == 400
    assert parse_access_line("Engine 000: Avg prompt throughput: 0.0 tokens/s", now=1.0) is None

def test_logtailer_follows_file(tmp_path):
    p = tmp_path / "log.txt"
    p.write_text("")
    got = []
    tail = LogTailer(str(p), on_event=got.append,
                     parse=lambda ln: TeeEvent(ts=0.0, kind="note", text=ln) if ln else None)

    async def go():
        tail.start()
        await asyncio.sleep(0.1)
        with p.open("a") as f:
            f.write("hello\n")
            f.flush()
        await asyncio.sleep(0.5)
        await tail.stop()

    asyncio.run(go())
    assert any(e.text == "hello" for e in got)
```
- [ ] Implement `providers/logsource.py`:
```python
from __future__ import annotations
import asyncio
import re
import time
from collections.abc import Callable
from pathlib import Path

from vllmstat.core.tee import TeeEvent

_ACCESS = re.compile(r'"(?P<method>[A-Z]+)\s+(?P<path>\S+)\s+HTTP/[\d.]+"\s+(?P<status>\d{3})')
_CLIENT = re.compile(r'(?P<client>\d{1,3}(?:\.\d{1,3}){3}|\[[0-9a-fA-F:]+\]):\d+\s+-\s+"')
_FILTER = ("/health", "/metrics", "/ping", "/v1/models")


def parse_access_line(line, *, now=None, filter_paths=_FILTER) -> TeeEvent | None:
    m = _ACCESS.search(line)
    if not m:
        return None
    path = m.group("path")
    if path.split("?", 1)[0] in filter_paths:
        return None
    cm = _CLIENT.search(line)
    client = cm.group("client").strip("[]") if cm else None
    return TeeEvent(
        ts=now if now is not None else time.time(),
        kind="http",
        method=m.group("method"),
        path=path,
        status=int(m.group("status")),
        client=client,
    )


class LogTailer:
    """Best-effort async tail of a log source. Never raises into the loop."""

    def __init__(self, source: str, *, on_event: Callable[[TeeEvent], None], parse=parse_access_line):
        self.source = source
        self._on_event = on_event
        self._parse = parse
        self._task: asyncio.Task | None = None
        self._proc: asyncio.subprocess.Process | None = None
        self._stop = False

    def start(self) -> None:
        self._task = asyncio.ensure_future(self._run())

    async def _run(self) -> None:
        try:
            if self.source.startswith("docker:"):
                await self._run_docker(self.source[len("docker:") :])
            else:
                await self._run_file(self.source)
        except asyncio.CancelledError:
            raise
        except Exception as e:  # noqa: BLE001 - surface as a note, never crash
            self._note(f"tee source error: {e}")

    async def _run_docker(self, name: str) -> None:
        try:
            self._proc = await asyncio.create_subprocess_exec(
                "docker", "logs", "-f", "--since", "0s", name,
                stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
            )
        except FileNotFoundError:
            self._note("docker not found")
            return
        assert self._proc.stdout is not None
        while not self._stop:
            raw = await self._proc.stdout.readline()
            if not raw:
                break
            self._feed(raw.decode("utf-8", "replace").rstrip("\n"))

    async def _run_file(self, path: str) -> None:
        p = Path(path).expanduser()
        while not self._stop:
            try:
                with p.open("r", errors="replace") as f:
                    f.seek(0, 2)
                    while not self._stop:
                        line = f.readline()
                        if line:
                            self._feed(line.rstrip("\n"))
                        else:
                            await asyncio.sleep(0.2)
                            if p.stat().st_size < f.tell():  # rotated/truncated
                                break
            except FileNotFoundError:
                self._note(f"waiting for {path}…")
                await asyncio.sleep(1.0)

    def _feed(self, line: str) -> None:
        ev = self._parse(line)
        if ev is not None:
            self._on_event(ev)

    def _note(self, text: str) -> None:
        self._on_event(TeeEvent(ts=time.time(), kind="note", text=text))

    def terminate(self) -> None:
        """Synchronous best-effort shutdown for app exit."""
        self._stop = True
        if self._proc is not None:
            try:
                self._proc.terminate()
            except ProcessLookupError:
                pass
        if self._task is not None:
            self._task.cancel()

    async def stop(self) -> None:
        self.terminate()
        if self._task is not None:
            try:
                await self._task
            except asyncio.CancelledError:
                pass
```
- [ ] Commit: `feat(tee): uvicorn access-line parser + async LogTailer (docker/file)`.

---

## Task 3: `render.tee`

**Files:** Modify `src/vllmstat/render.py`; Test `tests/test_render.py`.

- [ ] Tests:
```python
def test_render_tee_http_and_empty():
    from vllmstat import render
    from vllmstat.core.tee import TeeEvent
    assert "waiting" in render.tee([], width=60, source_desc="docker:x")
    out = render.tee([TeeEvent(ts=0.0, kind="http", method="POST",
                               path="/v1/chat/completions", status=200)],
                     width=60, source_desc="docker:x")
    assert "TEE" in out and "POST" in out and "/v1/chat/completions" in out and "200" in out

def test_render_tee_marks_errors_and_exchange():
    from vllmstat import render
    from vllmstat.core.tee import TeeEvent
    err = render.tee([TeeEvent(ts=0.0, kind="http", method="GET", path="/x", status=503)], width=40)
    assert "!" in err
    ex = render.tee([TeeEvent(ts=0.0, kind="exchange", prompt="hi there", response="hello", done=False)],
                    width=40)
    assert "▶" in ex and "◀" in ex
```
- [ ] Implement in `render.py` (add `import time`):
```python
def _tee_one_line(text: str, width: int) -> str:
    s = " ".join((text or "").split())
    return s if len(s) <= width else s[: max(1, width - 1)] + "…"


def tee(events, *, width=None, height=10, source_desc="") -> str:
    w = width or _DEFAULT_PLOT_WIDTH
    head = f"TEE · {source_desc or '—'}"
    rows: list[str] = []
    for e in events:
        if e.kind == "http":
            ts = time.strftime("%H:%M:%S", time.localtime(e.ts))
            mark = "!" if (e.status or 0) >= 400 else " "
            rows.append(_tee_one_line(f"{mark}{ts} {e.method or '?':<4} {e.path or ''}  {e.status or ''}", w))
        elif e.kind == "exchange":
            rows.append(_tee_one_line(f"▶ {e.prompt or ''}", w))
            rows.append(_tee_one_line(f"◀ {e.response or ''}{'' if e.done else ' ▌'}", w))
        else:  # note
            rows.append(_tee_one_line(f"· {e.text or ''}", w))
    if not rows:
        return f"{head}\n (waiting for requests…)"
    return head + "\n" + "\n".join(rows[-(height - 1) :])
```
- [ ] Commit: `feat(tee): TEE panel renderer`.

---

## Task 4: data plumbing (`logs` field, `--logs`, config, discovery, runtime buffer)

**Files:** Modify `core/state.py`, `config.py`, `core/resolve.py`, `providers/discover_docker.py`,
`core/fleet.py`; Tests in the matching test files.

- [ ] `core/state.py`: add `logs: str | None = None` to `Instance`. Test default is None.
- [ ] `config.py`: add `logs: str | None = None` to `Config`; arg `p.add_argument("--logs", dest="logs", default=None)`; set in `from_sources`. Test `--logs docker:x` → `cfg.logs == "docker:x"`.
- [ ] `core/resolve.py`: in `instance_from_dict`, read `logs=raw.get("logs")`. Test a config dict with `logs` → `Instance.logs` set.
- [ ] `providers/discover_docker.py`: in `build_instances`, set `logs=f"docker:{name}"` on each discovered Instance (so `--discover-docker` auto-tails). Update the existing discovery test to assert `out[0].logs == "docker:qwen"` (name without leading slash).
- [ ] `core/fleet.py`: `InstanceRuntime.__init__` adds `self.tee = TeeBuffer()` (import `from vllmstat.core.tee import TeeBuffer`). Test `InstanceRuntime(Instance("a","http://x")).tee` is a `TeeBuffer` with `len()==0`.
- [ ] Also wire the single/default instance's `logs`: this is consumed in Task 5 (app builds the default instance with `logs=cfg.logs`).
- [ ] Commit: `feat(tee): logs source on Instance/Config/discovery + runtime TeeBuffer`.

---

## Task 5: app wiring — TEE panel, `t` toggle, tailer lifecycle

**Files:** Modify `src/vllmstat/app.py`; Test `tests/test_app.py`.

- [ ] Tests:
```python
@pytest.mark.asyncio
async def test_tee_panel_hidden_without_source():
    cfg = Config(mock=True, interval=0.1, gpu=False)
    app = VllmStatApp(cfg)
    async with app.run_test() as pilot:
        await pilot.pause(0.2)
        assert app.query_one("#tee").display is False

@pytest.mark.asyncio
async def test_tee_panel_shows_with_source_and_toggles():
    cfg = Config(mock=True, interval=0.1, gpu=False)
    cfg.logs = "docker:does-not-exist"          # tailer emits a note, never crashes
    app = VllmStatApp(cfg)
    async with app.run_test() as pilot:
        await pilot.pause(0.2)
        assert app.query_one("#tee").display is True
        await pilot.press("t")
        assert app.query_one("#tee").display is False
        await pilot.press("t")
        assert app.query_one("#tee").display is True
```
- [ ] Implement in `app.py`:
  - Imports: `from vllmstat.providers.logsource import LogTailer`.
  - `__init__`: the default single instance gains `logs=cfg.logs`; add `self.tee_visible = True`;
    `self._tailers: list[LogTailer] = []`.
  - `compose`: add `self.p_tee = Panel(id="tee")` as the **last** child inside the `#detail`
    `Vertical` (after `p_gpu`).
  - CSS: add `#tee { height: auto; }`.
  - `on_mount`: after `_apply_mode`, start tailers — `for rt in self.fleet.runtimes: if rt.instance.logs: t = LogTailer(rt.instance.logs, on_event=rt.tee.push); t.start(); self._tailers.append(t)`.
  - `on_unmount` (sync): `for t in self._tailers: t.terminate()`.
  - BINDINGS: add `("t", "toggle_tee", "Tee")`.
  - `action_toggle_tee`: flip `self.tee_visible`, then `self._refresh()`.
  - `_refresh_detail(inst, snap, hist)`: after updating the existing panels, drive the TEE panel:
    ```python
    rt = self.fleet.runtimes[min(self.selected, len(self.fleet.runtimes) - 1)]
    has_tee = bool(inst.logs) or len(rt.tee) > 0
    self.p_tee.display = has_tee and self.tee_visible
    if self.p_tee.display:
        self.p_tee.update(render.tee(
            rt.tee.recent(40), width=self._panel_width(self.p_tee),
            source_desc=inst.logs or "proxy", height=12,
        ))
    ```
    (Ensure `_current()`/selection index matches the runtime used here. In single mode selected=0.)
  - When the overview is shown (fleet, not in detail), the `#tee` panel is inside `#detail` which is
    already hidden by `_apply_mode`, so no extra handling is needed. But make sure `p_tee.display` is
    set to False when there is no source so it doesn't reserve space.
- [ ] Run `pytest tests/test_app.py -q` then full `pytest -q`. Keep all prior app tests green
  (the new `#tee` panel must not disturb single/fleet mounting or the GPU regression test).
- [ ] Commit: `feat(tee): TEE panel in the dashboard with t-toggle and tailer lifecycle`.

---

## Task 6 (controller): docs, version, screenshot, release

- [ ] Bump to `0.4.0` (pyproject + `__init__`).
- [ ] README: new **Tee / request feed** section — `--logs docker:NAME` / `--logs FILE` / config
  `logs=` / auto via `--discover-docker`; explain it's an HTTP request feed (no content on vLLM V1) and
  that Phase 2 proxy mode (coming) adds full prompt/response tee; add `t` to the key-bindings table and
  `--logs` to the flags table.
- [ ] New screenshot of the dashboard with the TEE panel (Rich SVG → librsvg PNG), or reuse the live
  docker feed.
- [ ] Full `ruff && pyright && pytest` green; final review; merge to main; build; twine upload `0.4.0`.

## Self-review notes
- `TeeEvent` is general enough for Phase 2 `exchange` events (already has prompt/response/streaming).
- The `#tee` panel lives inside `#detail`, so fleet overview hides it automatically.
- LogTailer is best-effort: a missing docker/container/file emits a `note`, never an exception.
