Files
crossbar/docs/plans/v2/02-ctxguard.md
T
kyleandClaude Fable 5.1 74b8af4ce0 AGENTS.md: a refused tool call is not a reason to end the turn; v2 plan (props, ctx guard, wake, identity, wiring) as acceptance tests; v1.1 run note
v2 given tests compiled against a panic-only skeleton (go vet clean); no reference
implementation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 08:37:38 -07:00

68 lines
3.3 KiB
Markdown

# v2 task 02: the context-size guard
**Branch:** `v2` (run `git switch v2`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Proxy: move or refuse prompts that do not fit the leased host's context`
## Goal
The single worst failure a client sees today is the upstream's "Context size has been exceeded"
after a long wait. With `PerSlotCtx` known (task 01), crossbar can estimate a prompt's size from
its body and act before forwarding: move the conversation to a host where it fits, or answer 400
with the estimate and the largest slot available. `PLAN.md` §4b.
## Files
- Copy: `internal/proxy/ctxguard_test.go`
- Modify: `internal/proxy/proxy.go` and/or `forward.go` (new file `ctxguard.go` if that keeps files under 400 lines), `internal/lease/lease.go` (one addition, below), `docs/implementer-log.md`
## Interfaces
```go
// internal/proxy
const CtxHeader = "X-Crossbar-Ctx" // set only when the guard moved a conversation: "moved:<from>>><to>" e.g. "moved:small>big"
// internal/lease — one addition, the only change allowed there:
// Move re-leases k onto host (deleting any existing lease for k), records a LeaseEvent with
// Reason "ctx", FromHost the previous host ("" if none), ToHost host. Returns an error only from
// the persister.
func (t *Table) Move(k Key, host string, now time.Time) error
```
Rules the tests check (`ServeHTTP`, between acquiring the lease and taking a slot):
1. `estimate := int(float64(len(body)) / 4 * 1.2)` tokens (body = the bytes already peeked; GET/HEAD → 0).
2. `limit := PerSlotCtx` of the leased host's `health.Status`. If `limit == 0` (unknown) or
`estimate <= limit`: no action, no header.
3. Otherwise find, among the route's candidate hosts (in order), the healthy, non-draining hosts
whose `PerSlotCtx() >= estimate` — prefer one that lists the model as loaded, else one that
can serve it (`cfg.Serves`). If one exists: `leases.Move(key, host, now)`, set
`CtxHeader` to `moved:<old>><new>`, and continue with the new host (**this request and the
following turns**: the lease moved).
4. If none exists: **400** `{"error":"prompt too large","estimate":E,"max":M}` where `M` is the
largest `PerSlotCtx()` among the route's healthy hosts (0 if all unknown — but then rule 2
already let the request through). Record an accounting row with status 400 and
`Err: "prompt too large"`; do not mark anything down; do not forward.
5. The estimate is never logged with the body; the log line gains `ctx_est=E` only.
Add `ReasonCtx = "ctx"` to `internal/store` constants (one-line change, allowed).
## Steps
- [ ] **1.** `git switch v2`; `cp docs/plans/v2/_files/internal/proxy/ctxguard_test.go internal/proxy/`.
- [ ] **2. See it fail** (compile: `CtxHeader`). **3. Write the code.** `gofmt -w internal/`.
- [ ] **4.** `go test -race -count=2 ./internal/proxy/ ./internal/lease/` → `ok`.
- [ ] **5.** `make gate` → `gate: ok`. **6.** Row `v2/02-ctxguard`; commit.
```sh
git add internal/proxy internal/lease internal/store docs/implementer-log.md
git commit
```
## Done when
- Tests pass with `-race -count=2`; gate ok; the copied test is byte-identical.
## Stop and report if
- `TestStickyLeaseSurvivesGrowthUntilItDoesNotFit` fails on the *second* request after the move (the lease did not actually move): quote the lease table.