# P2 Code Review — Fix Report

**Date:** 2026-06-12
**Status:** ALL BLOCKING FINDINGS FIXED

---

## B1 (REFUTED by supervisor) — No action taken.

The reviewer claimed routes need prefix changes. The supervisor correctly noted that `control-proxy.ts` rewrites `/api/control/*` to `/api/*`, so the control service routes are correct as-is.

---

## B2 (FIXED) — jobType 'action' as any

**Problem:** `actions.ts:70` used `jobType: 'action' as any`, violating the contract enum `['bench', 'eval']`. The web type guard silently dropped every action job frame.

**Fix:**
- `packages/contracts/src/ws-frames.ts:548` — added `'action'` to `z.enum(['bench', 'eval', 'action'])`
- `apps/web/src/api/types.ts:591` — mirrored: `jobType: 'bench' | 'eval' | 'action'`
- `apps/web/src/hooks/useControlStream.tsx:166` — type guard: `['bench', 'eval', 'action'].includes(...)`
- `apps/web/src/hooks/useControlStream.tsx:180` — ControlStreamState jobs type updated
- `apps/control/src/routes/actions.ts:70` — `as any` removed, now `as const`
- Rebuilt contracts: `pnpm -C packages/contracts build`

**Verification:** contracts test (29 tests), control build, web tsc --noEmit all pass.

---

## B3 (FIXED) — rebuildFleetFromDB iteration order

**Problem:** Model events queried `ORDER BY ts DESC` so older rows overwrite newest state in the Map.

**Fix:** `apps/control/src/index.ts:274` — changed to `ORDER BY ts ASC`. With ASC iteration, `Map.set()` overwrites with the latest state for each model, so the newest event wins.

---

## B4 (FIXED) — ttlDeadline recalculation

**Problem:** Rebuild computed `new Date(Date.now() + ttl * 1000)`, giving models a fresh TTL from rebuild time instead of from event time.

**Fix:** `apps/control/src/index.ts:297-299` — changed to `new Date(eventTs + ttl * 1000)` where `eventTs = new Date(row.ts).getTime()`. This matches the semantic intent: the deadline reflects when the model was actually loaded, not when we rebuild.

**Evidence:** The live handler (`index.ts:57`) does `new Date(Date.now() + ttl * 1000)` relative to event arrival. The rebuild now uses the event timestamp, which is the correct reference point for a historical event.

---

## B5 (FIXED) — currentEventType resets between network chunks

**Problem:** `fleet-connector.ts:204` declared `currentEventType` inside the chunk-read loop, so an `event:` line in one network chunk and its `data:` line in the next lost the event type.

**Fix:** `apps/control/src/services/fleet-connector.ts:196-198` — hoisted `let currentEventType: string | null = null` outside the `while (!signal.aborted)` read loop, making it connection-scoped. Added comment explaining the rationale.

---

## B6 (FIXED) — late joiners never receive log tail

**Problem:** WS connect sends fleet snapshot but never replays the in-memory LogRelay tail.

**Fix:**
- `apps/control/src/routes/ws.ts` — `registerControlWebSocket` now accepts `logRelay: LogRelay | null` parameter
- After sending the fleet snapshot, iterates `logRelay.getAllTails()` and sends each as a `control_log` frame
- `apps/control/src/index.ts:363` — passes `logRelay` to `registerControlWebSocket`

---

## B7 (FIXED) — capture string interpolation into ::jsonb

**Problem:** `index.ts:120` did `${captureTrimmed ? sql\`'\${captureTrimmed}'::jsonb\` : ...}`, which interpolates a JSON string into a quoted ::jsonb fragment, producing double-serialized storage.

**Fix:**
- `apps/control/src/services/retention.ts` — added `parseCaptureJson()` that parses the trimmed string into an object (or null for invalid JSON)
- `apps/control/src/index.ts:118-122` — pipeline: `trimCapture()` -> `parseCaptureJson()` -> `sql.json(parsedObj as never)` per convention
- Added test in `retention.test.ts` asserting the parsed result is an object suitable for `sql.json()`, not a string
- Also fixed `trimCapture` to use `Buffer.byteLength` instead of `length * 2` for accurate byte counting

---

## B8 (CONFIRMED + FIXED) — 'model' source log lines silently dropped

**Trace:**
1. `index.ts:103` — publishes `source: event.data.source as 'proxy' | 'upstream'` (cast is no-op at runtime; 'model' passes through)
2. `ws-frames.ts:540` — contracts enum was `['proxy', 'upstream']` only
3. `useControlStream.tsx:155` — type guard checked `['proxy', 'upstream'].includes(...)` — 'model' fails
4. Frame silently dropped at the JSON parse boundary

**Fix (end-to-end):**
- `packages/contracts/src/ws-frames.ts:540` — `z.enum(['proxy', 'upstream', 'model'])`
- `apps/web/src/api/types.ts:584` — `source: 'proxy' | 'upstream' | 'model'`
- `apps/web/src/hooks/useControlStream.tsx:47` — `ControlLogEntry.source` widened
- `apps/web/src/hooks/useControlStream.tsx:75` — `ControlLogFrame.source` widened
- `apps/web/src/hooks/useControlStream.tsx:155` — type guard: `['proxy', 'upstream', 'model'].includes(...)`
- `apps/control/src/index.ts:103` — source cast widened to include 'model'

---

## A1 (FIXED) — handleReconcile swallows errors

**Problem:** `index.ts:112-114` — `.catch(() => { /* DB failure must not crash the process. */ })`

**Fix:** `apps/control/src/index.ts:112-115` — logs the error: `console.warn({ providerId, err: msg }, 'fleet: reconcile failed')`

---

## Test results

```
contracts:  29 tests, 2 passed (29 passed)
control:    74 tests, 10 passed (74 passed)
server:    575 tests, 50 passed | 2 skipped (586 total)
web tsc:    0 errors (clean)
```

## Files changed (this batch)

| File | Change |
|------|--------|
| `packages/contracts/src/ws-frames.ts` | B2: 'action' to jobType; B8: 'model' to source |
| `apps/web/src/api/types.ts` | B2+B8: mirrored enums |
| `apps/web/src/hooks/useControlStream.tsx` | B2+B8: type guards + ControlStreamState |
| `apps/control/src/routes/actions.ts` | B2: removed `as any` |
| `apps/control/src/index.ts` | B3: ASC order; B4: eventTs ttlDeadline; B7: sql.json; A1: error log |
| `apps/control/src/services/fleet-connector.ts` | B5: hoisted currentEventType |
| `apps/control/src/routes/ws.ts` | B6: logRelay replay on connect |
| `apps/control/src/services/retention.ts` | B7: parseCaptureJson + byteLength fix |
| `apps/control/src/services/__tests__/retention.test.ts` | B7: JSONB object test |
