Why
A coder run on the local lane (--model ollama:...) leaves no record on this server: the CLI keeps Ollama messages local and sends nothing to the threads API. But the server is where everything rehydrates from — /threads, /export, resume, the Gym's live transcripts (#241, #242), and leaderboard accounting. A lane that leaves no record is invisible to all of it.
Today POST /api/v1/threads refuses this shape twice over: the model must be an admitted catalog id with an available provider (PROVIDER-002), and every open mints a grant. A local model is neither in the catalog nor metered.
What
Admit a transcript-only thread: POST /api/v1/threads with "lane": "local" and a bounded free-form model string (for example ollama:qwen3.8:27b-mtp-q8_0).
- No grant is minted, ever —
mint_grant/1 must refuse a local-lane thread the way it refuses a terminal one, so the no-provider-key and metering invariants hold by construction. The create response carries no token.
- No catalog or provider validation for the model string; it is recorded as the vendor string it is, bounded like other fields.
- Everything else is an ordinary thread: events append through
POST /api/v1/threads/{id}/events, /threads/:id renders and streams it, visibility tiers apply, the open-thread cap still counts it, delete revokes it.
- Update
INVARIANTS.md where the thread/grant invariants are stated, with tests: a local-lane thread holds no grant and cannot be granted; a grant-bearing path never accepts a non-catalog model.
Acceptance
- Opening a local-lane thread returns the thread without a grant; requesting a grant for it is refused.
- Events recorded against it stream on
/threads/:id exactly as thread-lane events do.
- The existing thread-lane create path is byte-for-byte unchanged for existing callers.
mix precommit green.
Companion CLI work in the monorepo: the local lane opens one of these threads by default and writes its transcript through the same writer the thread lane uses. Feeds Gym streaming for local-lane trials (#241, #242).
Why
A coder run on the local lane (
--model ollama:...) leaves no record on this server: the CLI keeps Ollama messages local and sends nothing to the threads API. But the server is where everything rehydrates from —/threads,/export, resume, the Gym's live transcripts (#241, #242), and leaderboard accounting. A lane that leaves no record is invisible to all of it.Today
POST /api/v1/threadsrefuses this shape twice over: the model must be an admitted catalog id with an available provider (PROVIDER-002), and every open mints a grant. A local model is neither in the catalog nor metered.What
Admit a transcript-only thread:
POST /api/v1/threadswith"lane": "local"and a bounded free-form model string (for exampleollama:qwen3.8:27b-mtp-q8_0).mint_grant/1must refuse a local-lane thread the way it refuses a terminal one, so the no-provider-key and metering invariants hold by construction. The create response carries no token.POST /api/v1/threads/{id}/events,/threads/:idrenders and streams it, visibility tiers apply, the open-thread cap still counts it, delete revokes it.INVARIANTS.mdwhere the thread/grant invariants are stated, with tests: a local-lane thread holds no grant and cannot be granted; a grant-bearing path never accepts a non-catalog model.Acceptance
/threads/:idexactly as thread-lane events do.mix precommitgreen.Companion CLI work in the monorepo: the local lane opens one of these threads by default and writes its transcript through the same writer the thread lane uses. Feeds Gym streaming for local-lane trials (#241, #242).