# ADR-150: Run the PYQ-DPP pipeline on a VPS-native OmniRoute instance, not DL-only

## Status
Accepted — 2026-09-09
### Status History
- 2026-09-09: Accepted (Decision Maker: hkl; documented by claude, session close-out)

## Context

The DPP-from-PYQ generation pipeline (JEEGym cue extraction -> LLM concept-question generation via
OmniRoute's claude/claude-opus-5 route -> compose -> render -> Playwright verify) was approved by
hkl for a full 31-math-chapter production run. hkl was about to travel for two days with
unreliable/no internet access and could not guarantee DL (his laptop) would stay powered on and
online for the run's duration.

OmniRoute was, at the time, installed and OAuth-connected only on DL, exposing an
OpenAI/Anthropic-compatible API at 127.0.0.1:20128 backed by hkl's Claude Max subscription via a
claude-code OAuth connection. A direct curl from the VPS to DL's instance confirmed it unreachable
(DL sits behind NAT with no inbound path). This was initially read and stated to hkl as "OmniRoute
is DL-only, cannot be moved" -- a claim about DL-instance reachability, not about whether OmniRoute
itself could run as a standalone service elsewhere. hkl pushed back explicitly ("can we set this
for VPS?"), which prompted investigation rather than re-arguing the reachability test.

Investigation found OmniRoute is a portable npm package (omniroute, v3.8.50) with no DL-specific
dependency -- it can be installed and OAuth-connected independently on any host, including the VPS,
using the same Claude Max account.

## Decision

Install and OAuth-connect a second, independent OmniRoute instance directly on the VPS
(/root/.omniroute/), and run the PYQ-DPP production pipeline against that VPS-local instance
rather than against DL's. DL's OmniRoute instance remains in place for DL-local/interactive use.
Both instances authenticate against the same underlying Claude Max account
(360degreemktg@gmail.com) via separate OAuth grants and separate encrypted credential stores.

## Alternatives Considered

1. Keep the pipeline DL-only, harden it for outages instead. Rejected as the primary path: it
   satisfies "doesn't lose work if DL goes offline" but not hkl's actual goal, which was to consume
   the current weekly Claude Max quota window before it lapses on Thursday while he has no
   reasonable way to keep DL online during travel. A DL-resilience-only design was actually built
   first (429-vs-broken-call retry fix, resumable orchestrator) and is kept as defense in depth, but
   it does not replace this decision.
2. Tunnel the VPS's requests back through DL's existing OmniRoute instance (e.g. reverse SSH
   tunnel VPS -> DL). Rejected: still requires DL to be powered on and reachable for the run's
   entire duration, which is exactly the constraint being removed.
3. Give the VPS instance write access to DL's existing credential store instead of a fresh OAuth
   grant. Not attempted: OmniRoute's credential store is a local encrypted SQLite file
   (STORAGE_ENCRYPTION_KEY-protected); copying it out is a real secret-exfiltration action with no
   clear benefit over a second, independent, dashboard-issued grant on the same account.

## Consequences

Positive
- The production pipeline is now fully independent of DL's power/network state; hkl's laptop can be
  off or offline with zero effect on the run.
- The VPS's network path to Anthropic's API measured faster than DL's during the actual cutover
  (pipeline ran noticeably faster post-migration with identical prompts/model).
- Established a real, reusable pattern (portable OmniRoute install + OAuth) rather than a one-off
  workaround.

Negative / risks
- Two OmniRoute instances now draw against one shared weekly Claude Max quota with no coordination
  or throttling between them. If DL's instance is used interactively for unrelated work while the
  VPS instance is mid-run, both draw from the same pool with no visibility into the other's
  consumption. Mitigation: none implemented yet -- open item, tracked in project_dpp_bank.md.
- A fresh VPS install of OmniRoute defaulted to binding 0.0.0.0 with no API key required -- caught
  and fixed (OMNIROUTE_SERVER_HOST=127.0.0.1, REQUIRE_API_KEY=true) before the instance was used
  for anything, but any future re-install on a new host must re-check this default rather than
  assume the fix carries over.
- oauth start on a fresh instance requires a dashboard-issued "Management Access" key
  (management_key auth-kind header) -- a CLI connect --scope admin bearer token is NOT accepted
  and returns 401. This is an OmniRoute product behavior, not a bug, but it means the OAuth
  ceremony cannot be fully automated from a CLI-only session; the dashboard step must be completed
  by a human with browser access. Recorded as a fact in project_omniroute.md so a future session on
  either instance doesn't re-discover it via a failed 401.

## Related Decisions
None superseded. Complements the DL-resilience work in the same session (429-handling fix,
resumable orchestrator) which remains active as defense in depth for DL-side interactive use.

## References
- Session narrative: memory/session_2026_09_09_pyq_dpp_vps_migration_and_acejee_verify.md
- memory/project_omniroute.md
- memory/project_dpp_bank.md (open item: cross-instance quota throttling)
- Toolchain lesson #306 (this session): the "not possible" misdiagnosis and its correction
