llm-relay 0.84.0 → 0.84.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/availability.d.ts +1 -1
- package/dist/backend.d.ts +1 -1
- package/dist/circuit-breaker.d.ts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/config-types.d.ts +10 -10
- package/dist/config.d.ts +1 -1
- package/dist/dispatch-lane-stats.d.ts +1 -1
- package/dist/dispatch.d.ts +3 -3
- package/dist/hedge-trigger.d.ts +1 -1
- package/dist/lane-cadence.d.ts +1 -1
- package/dist/lane-manifest.d.ts +1 -1
- package/dist/lane-probe.d.ts +2 -2
- package/dist/latency-demotion.d.ts +1 -1
- package/dist/mcp/lane-runner.d.ts +1 -1
- package/dist/mcp/protocol.d.ts +1 -1
- package/dist/mcp/readonly-boundary.d.ts +1 -1
- package/dist/mcp/server.d.ts +4 -4
- package/dist/process-safety-net.d.ts +1 -1
- package/dist/responses-request.d.ts +1 -1
- package/dist/stream-commit.d.ts +1 -1
- package/dist/stream-pipeline.d.ts +1 -1
- package/dist/tool-dialects.d.ts +1 -1
- package/docs/README.md +43 -0
- package/package.json +1 -1
- package/scripts/install-skill.mjs +1 -0
- package/skills/llm-relay/SKILL.md +4 -0
- package/skills/llm-relay/references/lane-field-notes.md +202 -0
package/README.md
CHANGED
|
@@ -65,10 +65,17 @@ at login.
|
|
|
65
65
|
## Learn more
|
|
66
66
|
|
|
67
67
|
- [docs/reference.md](docs/reference.md) — full reference: config, routing, pools, offload,
|
|
68
|
-
repair, CLI, endpoints, and every caveat.
|
|
68
|
+
repair, CLI, endpoints, and every caveat. It opens with a table of contents.
|
|
69
|
+
- [docs/architecture.md](docs/architecture.md) — a map of the code, for a person who wants to
|
|
70
|
+
change it.
|
|
71
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) — how to set up, how to prove a change, and how to report a
|
|
72
|
+
problem without leaking a credential.
|
|
69
73
|
- [docs/subagent-routing.md](docs/subagent-routing.md) — offload design and wire evidence.
|
|
70
74
|
- [docs/pool-failover.md](docs/pool-failover.md) — how failover and health tracking behave.
|
|
71
75
|
- [docs/capability-sources.md](docs/capability-sources.md) — where capability scores come from.
|
|
72
76
|
- [docs/project-goals.md](docs/project-goals.md) — what this project is and is not.
|
|
73
77
|
|
|
74
|
-
`llm-relay help` lists the main commands.
|
|
78
|
+
`llm-relay help` lists the main commands. To contribute, start at
|
|
79
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) and [docs/architecture.md](docs/architecture.md).
|
|
80
|
+
[CLAUDE.md](CLAUDE.md) holds the same source map in full detail, written for an AI coding
|
|
81
|
+
assistant.
|
package/dist/availability.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Availability ladders — spec §5.1-5.3 of docs/quota-metering-spec-2026-08-16.md.
|
|
2
|
+
* Availability ladders — spec §5.1-5.3 of docs/history/quota-metering-spec-2026-08-16.md.
|
|
3
3
|
*
|
|
4
4
|
* `remaining` and `resetsAt` are resolved per (scope, axis, period) through fixed rungs, and
|
|
5
5
|
* staleness is handled HERE, at read time, as a pure function of the caller's clock — never by a
|
package/dist/backend.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare class DialectUnparseableError extends Error {
|
|
|
29
29
|
* fine. It is a config decision, so it is carried as a LOCAL failure — the walk must not fail over
|
|
30
30
|
* (re-asking N models to produce the same refused action) and the deployment must not be charged,
|
|
31
31
|
* the same rule under which a hard cap "never registers on the breaker — it is config, not
|
|
32
|
-
* health". See docs/dialect-rescue-destructive-refusal-2026-08-24.md.
|
|
32
|
+
* health". See docs/history/dialect-rescue-destructive-refusal-2026-08-24.md.
|
|
33
33
|
*/
|
|
34
34
|
export declare class DialectDestructiveError extends Error {
|
|
35
35
|
readonly dialect: string;
|
|
@@ -186,7 +186,7 @@ export declare const UNMEASURED_STABILITY = 50;
|
|
|
186
186
|
* ladder: a `Retry-After`, a 429 escalation and a 402 all still win where they applied before.
|
|
187
187
|
*
|
|
188
188
|
* Pure, so it is pinned directly rather than through the breaker's state machine.
|
|
189
|
-
* Evidence: `docs/latency-demotion-regression-2026-08-30.md` §3.
|
|
189
|
+
* Evidence: `docs/history/latency-demotion-regression-2026-08-30.md` §3.
|
|
190
190
|
*/
|
|
191
191
|
export declare function failureCooldown(elapsedMs: number): {
|
|
192
192
|
ms: number;
|
package/dist/cli.d.ts
CHANGED
|
@@ -403,7 +403,7 @@ export declare function renderCommand(invoke: {
|
|
|
403
403
|
* ⚠ `--probe` spawns lane commands as an explicit operator action — same precedent as
|
|
404
404
|
* `pools --probe` sending real completions. The request path reads the cached manifest and never
|
|
405
405
|
* spawns anything; outside it the only OTHER spawn site is the relay's background lane cadence
|
|
406
|
-
* (owner decision 2026-08-29, docs/quota-reprobe-design-2026-08-29.md). Without `--probe` this
|
|
406
|
+
* (owner decision 2026-08-29, docs/history/quota-reprobe-design-2026-08-29.md). Without `--probe` this
|
|
407
407
|
* just prints the cache.
|
|
408
408
|
*/
|
|
409
409
|
export declare function runLanes(): Promise<void>;
|
package/dist/config-types.d.ts
CHANGED
|
@@ -98,7 +98,7 @@ export type ThoughtSignatureMode = "none" | "sentinel";
|
|
|
98
98
|
* `thinking: {type:"disabled"}` — DeepSeek's thinking mode requires the prior turn's
|
|
99
99
|
* `reasoning_content` to be replayed on a multi-turn conversation (HTTP 400 otherwise), and
|
|
100
100
|
* this relay deliberately holds no store to round-trip it (see
|
|
101
|
-
* docs/deepseek-responses-truncation-2026-09-09.md), so the default must not think.
|
|
101
|
+
* docs/history/deepseek-responses-truncation-2026-09-09.md), so the default must not think.
|
|
102
102
|
*/
|
|
103
103
|
export type ReasoningMode = "none" | "deepseek";
|
|
104
104
|
/**
|
|
@@ -126,7 +126,7 @@ export interface ProviderCompatConfig {
|
|
|
126
126
|
*
|
|
127
127
|
* OpenCode Zen's contributor SKUs — Muse Spark 1.3 included — answer HTTP 500 on
|
|
128
128
|
* `/chat/completions` and on Zen's Anthropic-shaped `/messages`, and 200 only on `/responses`
|
|
129
|
-
* (measured 2026-09-04, `docs/muse-spark-1.3-opencode-zen-2026-09-04.md` rows 3 and 6-8). A third
|
|
129
|
+
* (measured 2026-09-04, `docs/history/muse-spark-1.3-opencode-zen-2026-09-04.md` rows 3 and 6-8). A third
|
|
130
130
|
* `Kind` value for this would touch roughly 55 `kind === "openai"` sites across 19 files (same
|
|
131
131
|
* doc, §3 route B); this narrower option forks only the request/response builders `src/backend.ts`
|
|
132
132
|
* selects on, leaving discovery, catalog and key-check paths unchanged. Declaring it on an
|
|
@@ -329,7 +329,7 @@ export interface Routing {
|
|
|
329
329
|
latency?: LatencyDemotionConfig;
|
|
330
330
|
/**
|
|
331
331
|
* Hedged attempts (owner proposal + decisions 2026-08-30,
|
|
332
|
-
* docs/hedged-attempts-design-2026-08-30.md §7). **Default ON**, and confined to deployments
|
|
332
|
+
* docs/history/hedged-attempts-design-2026-08-30.md §7). **Default ON**, and confined to deployments
|
|
333
333
|
* `assessCost()` calls FREE.
|
|
334
334
|
*
|
|
335
335
|
* ⚠ This is the FIRST behaviour here that does not merely reorder — it DUPLICATES a request onto
|
|
@@ -366,7 +366,7 @@ export interface Routing {
|
|
|
366
366
|
pacing?: PacingConfig;
|
|
367
367
|
/**
|
|
368
368
|
* Post-commit CRAWL abort (backlog item 18, built 2026-09-09 after
|
|
369
|
-
* `docs/post-commit-stall-measurement-2026-09-09.md` measured that both Claude Code and Codex
|
|
369
|
+
* `docs/history/post-commit-stall-measurement-2026-09-09.md` measured that both Claude Code and Codex
|
|
370
370
|
* retry a stream that goes bad after content has already arrived — Claude Code once, downgraded
|
|
371
371
|
* to non-streaming; Codex up to five times, staying streaming). A silent stall after commit is
|
|
372
372
|
* already caught by `withStallWatchdog` at `stallTimeoutMs`; this catches the case nothing else
|
|
@@ -381,7 +381,7 @@ export interface Routing {
|
|
|
381
381
|
crawl?: CrawlWatchdogConfig;
|
|
382
382
|
/**
|
|
383
383
|
* Background lane re-probing (owner decision 2026-08-29,
|
|
384
|
-
* docs/quota-reprobe-design-2026-08-29.md): keeping lane metadata fresh is the relay's own
|
|
384
|
+
* docs/history/quota-reprobe-design-2026-08-29.md): keeping lane metadata fresh is the relay's own
|
|
385
385
|
* job, the way the ping loop already does for HTTP. **Default ON** — catalog probes are
|
|
386
386
|
* metadata commands that spend no quota, and quota probes fire only for buckets carrying an
|
|
387
387
|
* ACTIVE recorded death (an alive lane is re-tested by real use for free). Boolean shorthand
|
|
@@ -391,7 +391,7 @@ export interface Routing {
|
|
|
391
391
|
laneProbe?: LaneProbeSettings;
|
|
392
392
|
/**
|
|
393
393
|
* The automatic dispatch lane WALK (owner request 2026-09-06,
|
|
394
|
-
* docs/dispatch-lane-walk-design-2026-09-06.md). **Default ON.**
|
|
394
|
+
* docs/history/dispatch-lane-walk-design-2026-09-06.md). **Default ON.**
|
|
395
395
|
*
|
|
396
396
|
* Before it, `dispatch` ran ONE lane and reported a failure when that lane was slow; the calling
|
|
397
397
|
* agent then picked the next lane by hand, which is the friction the owner reported. With it,
|
|
@@ -495,7 +495,7 @@ export interface LatencyDemotionConfig {
|
|
|
495
495
|
/**
|
|
496
496
|
* `routing.hedge` — start the NEXT candidate beside a slow in-flight attempt, instead of after it
|
|
497
497
|
* (owner proposal 2026-08-30; the four decisions are in
|
|
498
|
-
* `docs/hedged-attempts-design-2026-08-30.md` §7).
|
|
498
|
+
* `docs/history/hedged-attempts-design-2026-08-30.md` §7).
|
|
499
499
|
*
|
|
500
500
|
* **Default ON, free deployments only.** That is owner decision D1, taken against the
|
|
501
501
|
* recommendation of off-by-default. `assessCost()` treats an UNKNOWN price as paid, so the rule is
|
|
@@ -876,7 +876,7 @@ export interface DispatchWalkSettings {
|
|
|
876
876
|
* answer-mode call. Default 600000 (10 minutes).
|
|
877
877
|
*
|
|
878
878
|
* ⚠ Two floors because the two modes are two different populations
|
|
879
|
-
* (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §3): an answer-mode call is one HTTP round trip
|
|
879
|
+
* (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §3): an answer-mode call is one HTTP round trip
|
|
880
880
|
* that answers in seconds, while an agent-mode lane runs a whole tool loop for minutes. One floor
|
|
881
881
|
* fitted to both stopped every real agent task on `free-pool` at 90 s, because a burst of short
|
|
882
882
|
* answer-mode calls had set the lane's p80 to 39.5 s. Like `attemptMs` it is an operator budget,
|
|
@@ -977,7 +977,7 @@ export interface McpSettings {
|
|
|
977
977
|
/**
|
|
978
978
|
* Default `routing.mcp.blockingWaitMs` — 25 minutes.
|
|
979
979
|
*
|
|
980
|
-
* Measured and documented 2026-09-17 (`docs/mcp-host-timeouts-2026-09-17.md`): Claude Code's
|
|
980
|
+
* Measured and documented 2026-09-17 (`docs/history/mcp-host-timeouts-2026-09-17.md`): Claude Code's
|
|
981
981
|
* wall-clock tool limit (`MCP_TOOL_TIMEOUT`) defaults to about 28 hours, and a 240 s call succeeded
|
|
982
982
|
* headless with and without progress. Its stdio idle timeout is 30 minutes, and the documentation
|
|
983
983
|
* says a progress notification resets it. The default stays under 30 minutes so the call survives
|
|
@@ -993,7 +993,7 @@ export declare const DEFAULT_MCP_BLOCKING_WAIT_MS = 1500000;
|
|
|
993
993
|
* somewhere between 45 s and 100 s, and Codex's code-mode `exec` tool yields its script at 31.0 s
|
|
994
994
|
* with empty output ("Script running with cell ID N / Wall time 31.0 seconds"). The 2026-09-10
|
|
995
995
|
* transcript sweep counted 29 of 266 first Codex dispatch calls that lost their job id that way
|
|
996
|
-
* while this default was 40 s (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §8). 25 s sits under
|
|
996
|
+
* while this default was 40 s (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §8). 25 s sits under
|
|
997
997
|
* both. The tool description names the config key rather than this figure, so an operator override
|
|
998
998
|
* never leaves the text stale.
|
|
999
999
|
*/
|
package/dist/config.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export declare function resolveThoughtSignatureMode(p: {
|
|
|
48
48
|
* labelled provider fact in `src/` only while config can override it. DeepSeek's own API states its
|
|
49
49
|
* thinking/reasoning vocabulary (thinking ON by default; `thinking: {type:"disabled"}` /
|
|
50
50
|
* `reasoning_effort: low|high|max`; first-party evidence in
|
|
51
|
-
* docs/deepseek-responses-truncation-2026-09-09.md), so `api.deepseek.com` defaults to `"deepseek"`
|
|
51
|
+
* docs/history/deepseek-responses-truncation-2026-09-09.md), so `api.deepseek.com` defaults to `"deepseek"`
|
|
52
52
|
* and every other host to `"none"`. An explicit `compat.reasoning` wins in BOTH directions —
|
|
53
53
|
* `"none"` on deepseek, `"deepseek"` on anything else.
|
|
54
54
|
*/
|
|
@@ -29,7 +29,7 @@ export type DispatchLaneStatus = (typeof DISPATCH_LANE_STATUSES)[number];
|
|
|
29
29
|
* ⚠ It keys the lane-stats window since 2026-09-10, because the two are different populations: a
|
|
30
30
|
* burst of short answer-mode calls set `free-pool`'s p80 to 39.5 s, and that one shared window then
|
|
31
31
|
* cut every agent-mode task on the same lane at the 90 s floor
|
|
32
|
-
* (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §3). One `as const` array, the type indexed from
|
|
32
|
+
* (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §3). One `as const` array, the type indexed from
|
|
33
33
|
* it and the parser validating against it — the closed-union rule.
|
|
34
34
|
*/
|
|
35
35
|
export declare const DISPATCH_MODES: readonly ["agent", "answer"];
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -251,7 +251,7 @@ export interface DispatchLane {
|
|
|
251
251
|
*
|
|
252
252
|
* ⚠ It exists because an abandoned run leaves no duration sample, so a window fed only by runs
|
|
253
253
|
* that finished inside the budget can never show that the lane needed longer — the floor lock
|
|
254
|
-
* measured on 2026-09-10 (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §3b). Each abandonment
|
|
254
|
+
* measured on 2026-09-10 (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §3b). Each abandonment
|
|
255
255
|
* doubles the next budget, capped at `MAX_ATTEMPT_BUDGET_MS`; one success resets the count.
|
|
256
256
|
*/
|
|
257
257
|
raisedBy?: number;
|
|
@@ -393,7 +393,7 @@ export interface DispatchOptions {
|
|
|
393
393
|
* WHO is asking, when that changes what can run. `"mcp"` is the `llm-relay mcp` server: it runs
|
|
394
394
|
* lanes itself and has no `Agent` tool, so a pass-through relay rung — one that forwards the
|
|
395
395
|
* caller's own Anthropic credential — can never run there. It comes back `unreachable`, never as a
|
|
396
|
-
* lane the walk tries and fails in 0 s (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §4).
|
|
396
|
+
* lane the walk tries and fails in 0 s (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §4).
|
|
397
397
|
* Absent ⇒ exactly the behaviour before this existed.
|
|
398
398
|
*/
|
|
399
399
|
requester?: "mcp";
|
|
@@ -407,7 +407,7 @@ export interface DispatchOptions {
|
|
|
407
407
|
* instead of the ladder. Validated against the configured providers and pools: an unknown spec
|
|
408
408
|
* yields no lane and a reason. `dispatch` could not name a model before 2026-09-10, so agents that
|
|
409
409
|
* had to use DeepSeek wrote their own HTTP calls to the relay
|
|
410
|
-
* (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §7).
|
|
410
|
+
* (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §7).
|
|
411
411
|
*/
|
|
412
412
|
model?: string;
|
|
413
413
|
}
|
package/dist/hedge-trigger.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* ⚠ **Hedging DUPLICATES, it does not reorder** — the first behaviour in this relay that does. The
|
|
13
13
|
* `CLAUDE.md` invariant reads *"Acting on counts is optional, always announced, and may only
|
|
14
14
|
* reorder"*, so the duplication is bounded three ways, all of them owner decisions recorded in
|
|
15
|
-
* `docs/hedged-attempts-design-2026-08-30.md` §7: it is confined to deployments `assessCost()`
|
|
15
|
+
* `docs/history/hedged-attempts-design-2026-08-30.md` §7: it is confined to deployments `assessCost()`
|
|
16
16
|
* calls FREE (D1), the loser is aborted the moment a winner commits, and the response announces it.
|
|
17
17
|
*
|
|
18
18
|
* WHY IT EXISTS. Measured 2026-08-30: `nim/deepseek-ai/deepseek-v4-flash-0731` hung on 43
|
package/dist/lane-cadence.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The background lane cadence — the relay's own re-probe loop for `cli` lanes.
|
|
3
3
|
*
|
|
4
|
-
* Owner decision 2026-08-29 (docs/quota-reprobe-design-2026-08-29.md): keeping lane metadata
|
|
4
|
+
* Owner decision 2026-08-29 (docs/history/quota-reprobe-design-2026-08-29.md): keeping lane metadata
|
|
5
5
|
* fresh is the relay's job, the way `PingLoop` already keeps HTTP health fresh — not a host
|
|
6
6
|
* scheduled task's, and never the nightly maintenance run's (that belongs to another repo).
|
|
7
7
|
* This closes the property the backlog demanded: a recorded quota death either expires on a
|
package/dist/lane-manifest.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* validated it. Measured 2026-08-08: the ladder handed an agent
|
|
6
6
|
* `agy --model claude-opus-5 --effort medium`, in which BOTH halves were wrong independently —
|
|
7
7
|
* AGY serves no Claude 5 at all, and `--effort` is rejected outright for its Claude models. The
|
|
8
|
-
* lane looked healthy and completed nothing. See docs/lane-discovery.md.
|
|
8
|
+
* lane looked healthy and completed nothing. See docs/history/lane-discovery.md.
|
|
9
9
|
*
|
|
10
10
|
* ⚠ A model the vendor does not serve is `not-servable` — an EXISTENCE fact, so the rung is
|
|
11
11
|
* REMOVED, not demoted. `target-facts.ts` already draws that line: `allowance-exhausted` demotes
|
package/dist/lane-probe.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Config } from "./config-types.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* `llm-relay lanes --probe` — ask each `cli` lane's own tool what it serves.
|
|
4
4
|
*
|
|
5
|
-
* ⚠ The spawn boundary (amended by owner decision 2026-08-29, docs/quota-reprobe-design-2026-08-29.md):
|
|
5
|
+
* ⚠ The spawn boundary (amended by owner decision 2026-08-29, docs/history/quota-reprobe-design-2026-08-29.md):
|
|
6
6
|
* the REQUEST PATH never runs a `cli` lane's command — a lane's quota is client-bound, it runs its
|
|
7
7
|
* own tool loop, and it returns only final text, so a relay that shelled out mid-request could
|
|
8
8
|
* never return the `tool_use` blocks an HTTP turn owes its caller. Outside the request path there
|
|
@@ -11,7 +11,7 @@ import type { Config } from "./config-types.js";
|
|
|
11
11
|
* metadata polling is the relay's job, exactly as the ping loop already does for HTTP. Nothing
|
|
12
12
|
* here is reachable from `handle()`, and the request path reads only the CACHED manifest.
|
|
13
13
|
*
|
|
14
|
-
* Discovery is not symmetric (docs/lane-discovery.md):
|
|
14
|
+
* Discovery is not symmetric (docs/history/lane-discovery.md):
|
|
15
15
|
* codex — `codex debug models` returns JSON including per-model `supported_reasoning_levels`,
|
|
16
16
|
* so both the id and the effort argument are validated with no API call spent.
|
|
17
17
|
* agy — `agy models` returns `id<TAB>label` and states nothing about flags, so its argument
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
* succeeding, inside a window every deployment passes through on its way to being measured.
|
|
70
70
|
* ⚠ A request sample with NO token count therefore reaches NEITHER statistic. That is deliberate:
|
|
71
71
|
* it is a generation of unknown length, so it is not normalisable and not what `p95Ms` describes.
|
|
72
|
-
* Evidence: `docs/latency-demotion-regression-2026-08-30.md`.
|
|
72
|
+
* Evidence: `docs/history/latency-demotion-regression-2026-08-30.md`.
|
|
73
73
|
*
|
|
74
74
|
* ⚠ **NO breaker cooldown is registered, and that is the design, not an omission.** Quota demotion
|
|
75
75
|
* can register one because its evidence STATES a `resetsAt`. Latency states no reset, and this
|
|
@@ -719,7 +719,7 @@ export interface CwdCheck {
|
|
|
719
719
|
* is offered, not imposed; that is the operator's call to make in config, not this file's.
|
|
720
720
|
*
|
|
721
721
|
* ⚠ **The containment test resolves BOTH sides with `path.resolve` before comparing** (closed
|
|
722
|
-
* 2026-09-03, docs/audit-findings-2026-09-03.md finding 1 / DR-002). Without it a literal `..`
|
|
722
|
+
* 2026-09-03, docs/history/audit-findings-2026-09-03.md finding 1 / DR-002). Without it a literal `..`
|
|
723
723
|
* segment in `cwd` — a raw string a caller sends verbatim, never normalized — satisfied a bare
|
|
724
724
|
* `startsWith` prefix test while `existsSync`/`statSync` above had already resolved `..` at the
|
|
725
725
|
* OS level against the REAL, escaped directory: `allowedRoots: ["C:/allowed"]` admitted
|
package/dist/mcp/protocol.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* ⚠ The known cost, stated so it is not rediscovered: hand-rolling means owning a spec that moves.
|
|
11
11
|
* The mitigation is that this file implements only the stable core. Extensions (Tasks, Apps,
|
|
12
|
-
* sampling) are deliberately absent — see `docs/mcp-dispatch-prior-art-2026-08-30.md` §3.1, which
|
|
12
|
+
* sampling) are deliberately absent — see `docs/history/mcp-dispatch-prior-art-2026-08-30.md` §3.1, which
|
|
13
13
|
* measured that no client ships Tasks support today.
|
|
14
14
|
*
|
|
15
15
|
* TRANSPORT. MCP stdio framing is one JSON message per line. A message therefore must not contain
|
|
@@ -22,7 +22,7 @@ export type ReadOnlyVerdict = {
|
|
|
22
22
|
* Containment is tested on RESOLVED paths with a separator boundary, never a bare `startsWith`:
|
|
23
23
|
* `C:/caller/tree-other` shares a prefix with `C:/caller/tree` and is not inside it, and a literal
|
|
24
24
|
* `..` segment resolves at the OS level before the comparison — the `checkCwd` defect closed
|
|
25
|
-
* 2026-09-03 (docs/audit-findings-2026-09-03.md finding 1 / DR-002), applied here in the direction
|
|
25
|
+
* 2026-09-03 (docs/history/audit-findings-2026-09-03.md finding 1 / DR-002), applied here in the direction
|
|
26
26
|
* where getting it wrong would WRONGLY PERMIT a mutation.
|
|
27
27
|
*
|
|
28
28
|
* Answer mode is always allowed: `startLane` skips the cwd/spawn path entirely for a `relay` rung
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -175,7 +175,7 @@ export declare const LANE_LADDER_PARTIAL_ADVICE: string;
|
|
|
175
175
|
* ⚠ The stopped lane did not fail on its own: the walk stopped it because it showed no activity the
|
|
176
176
|
* relay could see. So "every dispatch lane has now been tried" is false there, and would end the
|
|
177
177
|
* caller's use of dispatch for a task the lane might still finish
|
|
178
|
-
* (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §5). A NAMED lane is never stopped for idleness,
|
|
178
|
+
* (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §5). A NAMED lane is never stopped for idleness,
|
|
179
179
|
* so this names the call that lets it run to its own timeout.
|
|
180
180
|
*/
|
|
181
181
|
export declare function laneStoppedAdvice(laneId: string): string;
|
|
@@ -191,7 +191,7 @@ export declare const IDLE_POLL_MS = 15000;
|
|
|
191
191
|
* MCP clients (`initialize` `clientInfo.name`) measured to survive a long tool call, so `dispatch`
|
|
192
192
|
* may wait for the answer the way the host's own subagent does, up to `routing.mcp.blockingWaitMs`.
|
|
193
193
|
*
|
|
194
|
-
* Measured 2026-09-17 (`docs/mcp-host-timeouts-2026-09-17.md`): Claude Code 2.1.237 (`claude-code`)
|
|
194
|
+
* Measured 2026-09-17 (`docs/history/mcp-host-timeouts-2026-09-17.md`): Claude Code 2.1.237 (`claude-code`)
|
|
195
195
|
* completed a 240 s tool call headless, with and without progress. Two hosts are deliberately NOT
|
|
196
196
|
* here: the Claude desktop chat client (`claude-ai`) cancelled at exactly 60 s, and Codex runs a
|
|
197
197
|
* call inside a code-mode `exec` that yields at 31 s. An unknown client keeps `maxWaitMs`.
|
|
@@ -359,7 +359,7 @@ export declare class McpDispatchServer {
|
|
|
359
359
|
* streak of `LANE_UNRELIABLE_STREAK` own failures or more, or marked `failing`. Stopping a lane
|
|
360
360
|
* that may still answer in order to reach those trades an answer for a near-certain failure —
|
|
361
361
|
* measured 2026-09-10, when the walk stopped `free-pool` to try lanes that had answered 0 of 12,
|
|
362
|
-
* 0 of 34 and 0 of 21 runs (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §1). A lane with no
|
|
362
|
+
* 0 of 34 and 0 of 21 runs (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §1). A lane with no
|
|
363
363
|
* record counts as reliable: unmeasured is no opinion, never a failure.
|
|
364
364
|
*/
|
|
365
365
|
private stopWithheld;
|
|
@@ -445,7 +445,7 @@ export declare class McpDispatchServer {
|
|
|
445
445
|
* The quota death an AGY lane stated only in AGY's own log (`agy-quota-log.ts`). AGY retries a
|
|
446
446
|
* spent quota in silence, so a lane stopped by the walk or by its own timeout printed nothing, and
|
|
447
447
|
* the death reached the relay only when a run happened to last its whole length
|
|
448
|
-
* (`docs/dispatch-giveup-diagnosis-2026-09-10.md` §4). Undefined unless the lane is AGY, names its
|
|
448
|
+
* (`docs/history/dispatch-giveup-diagnosis-2026-09-10.md` §4). Undefined unless the lane is AGY, names its
|
|
449
449
|
* model, and the log is provably this run's — `agyQuotaStatement` refuses everything else.
|
|
450
450
|
*/
|
|
451
451
|
private agyLogReport;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* the same reasoning as an unset `${ENV}` disabling one provider instead of aborting startup.
|
|
11
11
|
*
|
|
12
12
|
* Design (fork-validated in freellmapi's process-safety-net, adopted 2026-08-13 — see
|
|
13
|
-
* docs/freellmapi-adoption-review-2026-08-13.md §1.3): swallow ONLY a closed allowlist of
|
|
13
|
+
* docs/history/freellmapi-adoption-review-2026-08-13.md §1.3): swallow ONLY a closed allowlist of
|
|
14
14
|
* transport error codes plus a short list of Node/undici-authored message shapes, and
|
|
15
15
|
* preserve Node's default fail-fast exit(1) for everything else, so genuine bugs still crash
|
|
16
16
|
* loudly. The classifier is a pure function so it is unit-testable without touching global
|
|
@@ -10,7 +10,7 @@ import type { ReasoningMode } from "./config-types.js";
|
|
|
10
10
|
* A TUNABLE DEFAULT, never a provider figure — the provenance invariant this whole module already
|
|
11
11
|
* follows. Until 2026-09-09 this module substituted llm-bridge's flat 1024 here UNCONDITIONALLY,
|
|
12
12
|
* even for an `openai`-kind target's outbound Chat body, where the field is genuinely optional.
|
|
13
|
-
* Measured against DeepSeek (docs/deepseek-responses-truncation-2026-09-09.md): 20 of 68 captured
|
|
13
|
+
* Measured against DeepSeek (docs/history/deepseek-responses-truncation-2026-09-09.md): 20 of 68 captured
|
|
14
14
|
* upstream answers ended `finish_reason: "length"` at EXACTLY `completion_tokens: 1024` — the
|
|
15
15
|
* relay's own invented cap, not anything the caller or the provider asked for — and every one of
|
|
16
16
|
* the 44 emitted `response.completed` events still carried `"status":"completed"`, so a cap
|
package/dist/stream-commit.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ export type FrontProtocol = "anthropic-messages" | "chat" | "responses";
|
|
|
105
105
|
* passthrough condition — `openai`-kind means translated on the Anthropic front, while on the
|
|
106
106
|
* OpenAI front only `openai`-kind PLUS `chat` is a passthrough. The two spellings never disagreed:
|
|
107
107
|
* a truth table over all six reachable combinations is in
|
|
108
|
-
* `docs/reviews/clone-07-clone-26-evidence-2026-09-05.md`, and `test/stream-commit.test.ts` pins
|
|
108
|
+
* `docs/history/reviews/clone-07-clone-26-evidence-2026-09-05.md`, and `test/stream-commit.test.ts` pins
|
|
109
109
|
* every row. Naming it once is what stops a fifth call site inventing a seventh row, because a new
|
|
110
110
|
* front or a new protocol currently has two places to get right and no compiler help.
|
|
111
111
|
*/
|
|
@@ -37,7 +37,7 @@ export declare function withStallWatchdog(response: Response, controller: AbortC
|
|
|
37
37
|
* Calibrated 2026-09-09, on the `latency-demotion.ts` precedent: 4 x `DEFAULT_LATENCY_MS_PER_TOKEN`
|
|
38
38
|
* (250 ms/token, itself measured over 68 real requests on 2026-08-30 — see that file's own
|
|
39
39
|
* comment). A crawl abort hands the client a failure it must retry — measured in
|
|
40
|
-
* `docs/post-commit-stall-measurement-2026-09-09.md`: Claude Code retries once, downgraded to a
|
|
40
|
+
* `docs/history/post-commit-stall-measurement-2026-09-09.md`: Claude Code retries once, downgraded to a
|
|
41
41
|
* NON-STREAMING request; Codex retries up to five times, staying streaming, in all four measured
|
|
42
42
|
* cells — so the bar must sit far above the demotion threshold, or a deployment merely slow enough
|
|
43
43
|
* to be latency-demoted would also be aborted mid-response. 250 ms/token is itself ~3.5x the
|
package/dist/tool-dialects.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ interface SchemaLike {
|
|
|
31
31
|
* upstream. An in-band error is provenance `upstream` by default and therefore retriable, which
|
|
32
32
|
* would let a streamed pre-commit refusal reroll onto the next candidate while the buffered lanes
|
|
33
33
|
* treat the same refusal as terminal — two paths, one policy. See
|
|
34
|
-
* docs/dialect-rescue-destructive-refusal-2026-08-24.md §3.
|
|
34
|
+
* docs/history/dialect-rescue-destructive-refusal-2026-08-24.md §3.
|
|
35
35
|
*/
|
|
36
36
|
export declare const DIALECT_REFUSED_DESTRUCTIVE_CODE = "tool_dialect_refused_destructive";
|
|
37
37
|
/**
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Documentation index
|
|
2
|
+
|
|
3
|
+
Start with the row that matches what you want to do.
|
|
4
|
+
|
|
5
|
+
| You want | Read |
|
|
6
|
+
|---|---|
|
|
7
|
+
| To install and run the relay | [`QUICKSTART.md`](QUICKSTART.md) |
|
|
8
|
+
| Every option, endpoint and caveat | [`reference.md`](reference.md) |
|
|
9
|
+
| To understand the code before changing it | [`architecture.md`](architecture.md) |
|
|
10
|
+
| To submit a change | [`../CONTRIBUTING.md`](../CONTRIBUTING.md) |
|
|
11
|
+
| To know what this project is, and is not | [`project-goals.md`](project-goals.md) |
|
|
12
|
+
| The convictions that settle a design question | [`project-philosophy.md`](project-philosophy.md) |
|
|
13
|
+
| What is still open | [`backlog.md`](backlog.md) |
|
|
14
|
+
|
|
15
|
+
## Subject documents
|
|
16
|
+
|
|
17
|
+
Each of these covers one subject in depth. Read one when you work on that subject.
|
|
18
|
+
|
|
19
|
+
| Document | Subject |
|
|
20
|
+
|---|---|
|
|
21
|
+
| [`pool-failover.md`](pool-failover.md) | how failover, health and circuit breaking behave |
|
|
22
|
+
| [`pool-eligibility.md`](pool-eligibility.md) | which deployments a pool may admit, and why |
|
|
23
|
+
| [`subagent-routing.md`](subagent-routing.md) | offload design, and the wire evidence behind it |
|
|
24
|
+
| [`host-adaptive-dispatch.md`](host-adaptive-dispatch.md) | how dispatch adapts to the calling host |
|
|
25
|
+
| [`offload-agentic-capability.md`](offload-agentic-capability.md) | what an offloaded agent lane can and cannot do |
|
|
26
|
+
| [`capability-sources.md`](capability-sources.md) | where capability scores come from |
|
|
27
|
+
| [`tool-call-dialect-leak.md`](tool-call-dialect-leak.md) | how a host leaks a tool call as plain text |
|
|
28
|
+
| [`delegate-gate.md`](delegate-gate.md) | the quality gate over a diff an agent lane returned |
|
|
29
|
+
|
|
30
|
+
## Data files
|
|
31
|
+
|
|
32
|
+
| File | Holds |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `tier-data.json` | the synced capability snapshot. Regenerate with `npm run sync:tiers`. |
|
|
35
|
+
| `dashboard-bundle-inventory.json` | the published dashboard bundle inventory |
|
|
36
|
+
| `dashboard-package-baseline.json` | the package-size ceiling the gate checks |
|
|
37
|
+
|
|
38
|
+
## Other directories
|
|
39
|
+
|
|
40
|
+
- [`history/`](history/) — dated records: design notes, audits and lap closeouts. These are
|
|
41
|
+
**evidence, not documentation**. Read [`history/README.md`](history/README.md) first.
|
|
42
|
+
- [`../CLAUDE.md`](../CLAUDE.md) — the full source map, written for an AI coding assistant. It
|
|
43
|
+
states the reason behind each rule. Read [`architecture.md`](architecture.md) first.
|
package/package.json
CHANGED
|
@@ -240,6 +240,7 @@ try {
|
|
|
240
240
|
join("references", "direct-routing.md"),
|
|
241
241
|
join("references", "dispatch-lanes.md"),
|
|
242
242
|
join("references", "operations.md"),
|
|
243
|
+
join("references", "lane-field-notes.md"),
|
|
243
244
|
];
|
|
244
245
|
for (const relativePath of skillFiles) {
|
|
245
246
|
const sourcePath = join(skillSourceDir, relativePath);
|
|
@@ -161,6 +161,10 @@ Do not read every reference by default.
|
|
|
161
161
|
read [references/dispatch-lanes.md](references/dispatch-lanes.md).
|
|
162
162
|
- For health commands, control authorization, failures, pool-walk headers, eligibility
|
|
163
163
|
interpretation, or safety invariants, read [references/operations.md](references/operations.md).
|
|
164
|
+
- Before you dispatch a packet, write a brief, judge a lane's output, run several lanes at once, or
|
|
165
|
+
diagnose a lost job, read [references/lane-field-notes.md](references/lane-field-notes.md). It
|
|
166
|
+
holds the measured traps: the CLI-rung reply envelope, lost job ids, restarts that kill lanes,
|
|
167
|
+
the concurrency cap, what each lane can carry, and why a brief's wording is not a boundary.
|
|
164
168
|
|
|
165
169
|
The CLI's `help`, `dispatch_lanes`, and live status commands are authoritative for current
|
|
166
170
|
configuration and quota. Do not copy a dated lane roster from prose.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Lane field notes — measured behaviour of llm-relay dispatch and its lanes
|
|
2
|
+
|
|
3
|
+
Every note here was MEASURED on this machine, with its date. They moved out of the machine-wide
|
|
4
|
+
backlog on 2026-09-17 (owner instruction: llm-relay-specific instructions belong with the llm-relay
|
|
5
|
+
skill, not in a shared to-do file). Each note is REFERENCE, not work: it is deleted when it becomes
|
|
6
|
+
untrue, never because something shipped. A defect in llm-relay itself belongs in
|
|
7
|
+
`C:\Code\llm-relay\docs\backlog.md` in product terms.
|
|
8
|
+
|
|
9
|
+
Ask the live tools first. `dispatch_lanes` and `llm-relay dispatch` carry the current lane record
|
|
10
|
+
and quota; never copy a dated roster out of prose.
|
|
11
|
+
|
|
12
|
+
## 1. Reading a dispatch reply
|
|
13
|
+
|
|
14
|
+
- **A `cli` rung's reply body is NOT the lane's answer (2026-09-16).** A `relay` rung such as
|
|
15
|
+
`free-pool` returns the raw answer. A `cli` rung such as `agy-gemini` returns its harness's own
|
|
16
|
+
record — `{"conversation_id":…,"status":"SUCCESS","response":"<the real answer>",
|
|
17
|
+
"duration_seconds":…,"usage":{…}}` — so the answer sits inside `response`. Code that binds the
|
|
18
|
+
body to a schema fails on exactly the calls the ladder sent to a CLI rung, and the failure looks
|
|
19
|
+
like a bad lane. Measured in audit-tools, 2026-09-16: 28 of 62 sweep calls lost, 19 to this
|
|
20
|
+
envelope. (llm-relay packet M1 will unwrap it in the relay and announce an `unwrapped:` line;
|
|
21
|
+
until that release, unwrap it yourself.)
|
|
22
|
+
- **A `SUCCESS` status is not an answer (2026-09-16, owner decided not to fix this in the relay).**
|
|
23
|
+
A `posttooluse-typecheck.mjs` dispatch (job-0021) returned `"status":"SUCCESS"` around 40 repeated
|
|
24
|
+
lines of "Waiting for test execution to complete." and no work product. Read the content by hand.
|
|
25
|
+
- **A completed job can carry no usable answer (2026-09-07 and 2026-09-09).** Agent-mode jobs
|
|
26
|
+
exited after 302 s and 623 s with the single words `Now` and `Let`, and their worktrees were
|
|
27
|
+
clean. Job `job-0005` exited 0 after 765 s and returned `I`. A review job completed after 813 s
|
|
28
|
+
with a completion claim and no findings. Treat a terminal status as evidence of TERMINATION only,
|
|
29
|
+
then inspect the answer and the worktree. None of these outcomes proves a provider quota is
|
|
30
|
+
spent.
|
|
31
|
+
- **`status: "running"` after a long `waitMs` is normal (2026-09-16).** The server clamps `waitMs`
|
|
32
|
+
to `routing.mcp.maxWaitMs`. Poll `dispatch_status`, then `dispatch_result`. In audit-tools 9 of
|
|
33
|
+
62 calls were lost by treating this as a fault.
|
|
34
|
+
- **Map a lane's verdict table to the file by HEADING, never by row number (2026-09-10).** A
|
|
35
|
+
read-only lane numbered 74 backlog entries out of file order (one moved from row 20 to row 8),
|
|
36
|
+
so its row numbers would have deleted the wrong entries. Re-derive rows with a script and match
|
|
37
|
+
each verdict by its heading text.
|
|
38
|
+
|
|
39
|
+
## 2. Waits, restarts and lost jobs
|
|
40
|
+
|
|
41
|
+
- **A large `waitMs` loses the job id (2026-09-06).** `waitMs: 180000` returned `Error: Request
|
|
42
|
+
timed out` with no job id, so the lane could not be polled, resumed or cancelled. The same task
|
|
43
|
+
at `waitMs: 40000` returned `job-0001` at once and finished in 145 s. Leave `waitMs` unset.
|
|
44
|
+
- **Codex's code-mode `exec` gives up on a tool call at 31.0 s (2026-09-10).** It returns "Wall
|
|
45
|
+
time 31.0 seconds" with empty output, so a longer MCP call loses its answer: 29 of 266 first
|
|
46
|
+
Codex `dispatch` calls, 2026-09-07 to 2026-09-10. Keep every MCP call from a Codex host under
|
|
47
|
+
30 s. llm-relay blocks 25 s by default since v0.81.0 and then hands back a job id.
|
|
48
|
+
Since v0.83.x the server waits longer only for hosts measured to survive it: Claude Code with a
|
|
49
|
+
progress token gets the answer in one call, Claude Desktop gets 50 s, every other host keeps the
|
|
50
|
+
ceiling (llm-relay `docs/history/mcp-host-timeouts-2026-09-17.md`).
|
|
51
|
+
- **An MCP server restart KILLS every lane it was running (2026-09-06, re-measured 2026-09-17).**
|
|
52
|
+
The old symptom — `unknown jobId` for every job, numbering restarted at `job-0001`, nothing on
|
|
53
|
+
disk — is fixed: the running-job journal reports each such job as `killed` (v0.80.0) and the job
|
|
54
|
+
archive keeps finished jobs and the id counter across restarts (v0.82.0). The WORK is still
|
|
55
|
+
lost: 14 of 83 archived jobs on 2026-09-17 were `killed`, 11 of them `agy-gemini`. Give every
|
|
56
|
+
lane its own worktree, because that directory is the only record of what a killed lane did, and
|
|
57
|
+
dispatch a killed job again.
|
|
58
|
+
- **One `llm-relay mcp` process can outlive a release (2026-09-10).** The Claude desktop app keeps
|
|
59
|
+
one MCP connection across its sessions, so after a global reinstall that connection runs the old
|
|
60
|
+
code until the app restarts. Since v0.81.0 a reply from a process older than the installed
|
|
61
|
+
package says so.
|
|
62
|
+
- **A session that hits its usage limit mid-turn loses every in-flight job (2026-09-04).** Three
|
|
63
|
+
`relay` subagents lost their lane jobs when the limit hit: the MCP connection was replaced, job
|
|
64
|
+
ids restarted at `job-0001`, and each agent saw `Request timed out` then `Connection closed`.
|
|
65
|
+
Keep the job handles in the main session rather than in subagents, and re-dispatch after the
|
|
66
|
+
reset.
|
|
67
|
+
- **An unrunnable lane is not a quota verdict (2026-09-09).** `Lane "anthropic" cannot be run from
|
|
68
|
+
here: it is a relay target (anthropic) with no cliLane template configured` means no answer was
|
|
69
|
+
produced. It says nothing about any account's quota. Keep the exact error.
|
|
70
|
+
|
|
71
|
+
## 3. Lane capacity and concurrency
|
|
72
|
+
|
|
73
|
+
- **Cap concurrent lanes at three or four; seven died together (2026-09-06).** Five
|
|
74
|
+
`opencode-muse-spark` and two `agy-gemini` lanes all hit the 2100 s timeout at the same moment,
|
|
75
|
+
exit 124, empty output, four packets lost — one had written 348 lines. Three lanes ran
|
|
76
|
+
comfortably afterwards. The relay does not cap this; the caller must.
|
|
77
|
+
- The tell is SIMULTANEITY. One silent lane is a lane problem; a cohort dying at the same
|
|
78
|
+
elapsed second is a load problem.
|
|
79
|
+
- The usual cause is asking every lane to run the full repository gate. Give a lane its TARGETED
|
|
80
|
+
suites plus lint, and keep the full gate for the orchestrating session.
|
|
81
|
+
- The orchestrator's own gate runs count toward the same budget: a cohort of only three lanes
|
|
82
|
+
also died together at 1800 s while the orchestrating session ran five full gates. Pause
|
|
83
|
+
dispatching while you verify, or drop to two lanes.
|
|
84
|
+
- A killed lane holds its worktree directory open, so `git worktree remove` reports "Permission
|
|
85
|
+
denied" although git DOES unregister the worktree. Believe `git worktree list`, not the
|
|
86
|
+
filesystem.
|
|
87
|
+
- A gate step that reaches the network flakes under this load: `npm audit` returned an error
|
|
88
|
+
payload while three lanes ran. Rerun such a step alone before believing it.
|
|
89
|
+
- **Two concurrent Muse Spark lanes starve, not only three (2026-09-09).** Two packets dispatched
|
|
90
|
+
together ran to the 2100 s timeout with empty output and no file written; single lanes finish in
|
|
91
|
+
minutes. Its rungs carry `maxConcurrent: 1` since llm-relay v0.78.0; keep it that way.
|
|
92
|
+
|
|
93
|
+
## 4. What each lane can carry
|
|
94
|
+
|
|
95
|
+
- **`opencode-muse-spark`** carries a whole implementation packet ALONE (101–998 s, 2026-09-09),
|
|
96
|
+
and starves as a second or third concurrent lane. Always pass `cwd`. Keep the task text under
|
|
97
|
+
4,096 characters and put a long brief in a file: a longer task makes the MCP server fall back to
|
|
98
|
+
its start-time config snapshot. It runs as the `relay-lane` OpenCode agent.
|
|
99
|
+
⚠ Headless OpenCode auto-rejects every permission set to `ask`, and the global default sets
|
|
100
|
+
`edit` and `bash` to `ask`, so without that agent a lane can read but cannot edit or run a suite,
|
|
101
|
+
and the failure looks like a model failure (measured 2026-09-04: `permission requested: edit …;
|
|
102
|
+
auto-rejecting`). A lane dispatched with no `cwd` had every READ rejected as
|
|
103
|
+
`external_directory`. The agent lives in `~/.config/opencode/opencode.json`; a repository-level
|
|
104
|
+
`opencode.json` merges over it, and `agent=<name>` on the run's `stream` lines in
|
|
105
|
+
`~/.local/share/opencode/log/opencode.log` is the only proof of which agent ran.
|
|
106
|
+
⚠ The agent did not end the zero-output mode (2026-09-04/05): with everything correct, two lanes
|
|
107
|
+
ran 918 s and 895 s and wrote zero bytes. Read-only recon on this lane is 35–70 s, so no file
|
|
108
|
+
change in the worktree after about five minutes is the signal to cancel and re-dispatch.
|
|
109
|
+
- **`agy-gemini`** is the steady CLI lane: 24 of 24 answered at a 240 s median on 2026-09-06, and
|
|
110
|
+
it carried packets at 348–561 s. It ran two lanes in one worktree (authoring plus a read-only
|
|
111
|
+
review) without interference.
|
|
112
|
+
⚠ It obeys an absolute path written INSIDE the brief over the `cwd` you passed, even when the
|
|
113
|
+
task says not to (2026-09-06). Never write an absolute worktree path into a shared brief, or
|
|
114
|
+
regenerate the brief per lane. Before concluding a lane produced nothing, look where its brief
|
|
115
|
+
pointed.
|
|
116
|
+
- **`agy-claude-opus`** drops the stream on long outputs (2026-09-04/05): `The stream was
|
|
117
|
+
interrupted` after a report summary, and `There was a network issue connecting to the server`
|
|
118
|
+
after 490 s. Split the work into packets and run them on `agy-gemini`.
|
|
119
|
+
- **Codex Spark** reads its whole usage window and writes nothing (2026-09-09, twice): 193k and
|
|
120
|
+
477k tokens, every test file read whole, then "You've hit your usage limit". A preamble limiting
|
|
121
|
+
reads changed nothing. Give it a review of a bounded diff, or nothing.
|
|
122
|
+
- **A shape that scored 10/10 yesterday is not a cure (2026-09-06).** Three `opencode-muse-spark`
|
|
123
|
+
lanes using the exact shape recorded as reliable the day before wrote nothing in 25 minutes while
|
|
124
|
+
`dispatch_status` said `running` and ten `opencode.exe` processes sat at about 500 MB each. Read
|
|
125
|
+
`dispatch_lanes` before choosing a lane; the same day it read 72 calls / 6 timed out / median
|
|
126
|
+
616 s for Muse Spark against 24 / 24 ok / median 240 s for `agy-gemini`.
|
|
127
|
+
|
|
128
|
+
## 5. Writing a brief, and trusting what comes back
|
|
129
|
+
|
|
130
|
+
- **A brief's wording is NOT a boundary (2026-09-06, measured twice).** A lane told to AUDIT a
|
|
131
|
+
closeout spent 19 minutes writing its own `closeout-input.json` into a live repository root, with
|
|
132
|
+
a fabricated verification section. Five later lanes each opened with "Do NOT edit any file.
|
|
133
|
+
Report findings only"; one still ran suites in the shared tree, wrote to the repository root,
|
|
134
|
+
performed the repository's own closeout ceremony, and an untracked deliverable of the
|
|
135
|
+
orchestrating session vanished at the same minute. The controls that DO work: give every writing
|
|
136
|
+
lane its own worktree, use `mode: "answer"` when the lane needs no file access, use
|
|
137
|
+
`readOnly: true` for an agent lane (llm-relay binds the lane's own read-only tool flags since
|
|
138
|
+
v0.82.0), commit an in-progress deliverable before dispatching into the same tree, and run
|
|
139
|
+
`git status --porcelain` after EVERY lane returns or is cancelled. A cancelled lane leaves its
|
|
140
|
+
files behind exactly like a completed one.
|
|
141
|
+
- **A brief that says "never print the key" does not stop a lane writing the key (2026-09-09).** A
|
|
142
|
+
capture lane put `export DEEPSEEK_API_KEY="sk-…"` into a scratch `start-relay.sh`. Tell the lane
|
|
143
|
+
to read a secret from the environment at run time and never copy the value into a file, and grep
|
|
144
|
+
every scratch launcher for `sk-` before running it or passing it on.
|
|
145
|
+
- **Ask a lane to EXTRACT, not to give a VERDICT (2026-09-06).** A free-pool lane given a rubric
|
|
146
|
+
and asked for `clear`/`defective` answered `clear` for all 97 records: a rubric whose rules
|
|
147
|
+
mostly say "this is not a defect" pushes a weak model to the null answer, and the output looks
|
|
148
|
+
well formed. The same job as an extraction — list the terms a cold reader cannot resolve, rate
|
|
149
|
+
0–10 — did not collapse. Always check a lane's label DISTRIBUTION before using its labels.
|
|
150
|
+
- **Well-formed output can still be wrong, and that is the version that gets believed
|
|
151
|
+
(2026-09-06).** A lane produced 120 clean records in the requested shape; against a hand-labelled
|
|
152
|
+
overlap its best agreement was 64%, while answering "clear" every time scored 73%. Never fold
|
|
153
|
+
lane labels into a count, a training set or a conclusion without measuring agreement on a
|
|
154
|
+
hand-labelled overlap, and always against the always-answer-the-majority baseline.
|
|
155
|
+
- **A lane that returns one large JSON object at the end returns NOTHING when it stops early
|
|
156
|
+
(2026-09-06, three lanes lost).** Have the lane append one JSON line per record as it works, and
|
|
157
|
+
slice the job to about 40 records rather than 100.
|
|
158
|
+
- **Free lanes cannot do open-ended reconnaissance here (2026-09-05, 7 of 7 packets fabricated).**
|
|
159
|
+
They CAN review a concrete diff against a stated claim, and they carry a mechanical rewrite with
|
|
160
|
+
a stated rule. The test is whether the output can be checked by running or reading something
|
|
161
|
+
specific.
|
|
162
|
+
|
|
163
|
+
## 6. Hooks, keys and the daemon
|
|
164
|
+
|
|
165
|
+
- **A relay lane loads NO global hook (2026-09-17).** `dispatch` launches each Claude lane with
|
|
166
|
+
`CLAUDE_CONFIG_DIR=~/.llm-relay-claude`, so the lane reads `~/.llm-relay-claude/settings.json`
|
|
167
|
+
and never `~/.claude/settings.json`. A global hook guards the orchestrator's own tool calls only.
|
|
168
|
+
Codex, OpenCode and AGY lanes have no hook surface at all; for them the orchestrator-side
|
|
169
|
+
`dispatch-cwd-guard.mjs` and the lane's own worktree are the only controls.
|
|
170
|
+
- **Provider API keys are NOT environment variables on this machine (2026-09-09).** `llm-relay
|
|
171
|
+
keys` prints an `Env var` column, which is the NAME the relay looks for, not proof the variable
|
|
172
|
+
exists: `NVIDIA_API_KEY` is empty in every scope while the same key reads `VALID`, because the
|
|
173
|
+
secret lives DPAPI-wrapped in `~/.llm-relay/keystore.json`. A direct `curl` therefore sends an
|
|
174
|
+
empty bearer, and NVIDIA answers HTTP 500 with a Rust `axum::Extension` message that reads like a
|
|
175
|
+
provider fault. Probe through the relay instead: `POST http://127.0.0.1:8791/v1/messages` with
|
|
176
|
+
`"model": "<provider>/<model id>"`. A public `/v1/models` answer proves nothing about a key.
|
|
177
|
+
- **A provider timeout turns a slow model into a fake "not servable" (2026-09-09).** With `nim` at
|
|
178
|
+
`timeoutMs: 100000`, a 32-token probe of `deepseek-ai/deepseek-v4-flash-0731` returned HTTP 504
|
|
179
|
+
at 100.03 s, while its sibling answered 200 after 81.6 s for two output tokens; the same Flash
|
|
180
|
+
model had answered in 38 s on 2026-08-27. A 504 at the configured timeout is evidence about the
|
|
181
|
+
QUEUE. Raise `providers.<name>.timeoutMs` and probe again before recording a model as dead.
|
|
182
|
+
`firstByteTimeoutMs` (v0.78.0) fails over fast when nothing arrives at all, while a slow body
|
|
183
|
+
still runs to its end.
|
|
184
|
+
- **The daemon reads `config.json` ONCE at start.** A rung or provider edit is invisible until the
|
|
185
|
+
daemon restarts — confirmed live: after a rung edit the `agy.exe` command line still carried the
|
|
186
|
+
old `--model`, and a re-probe after a timeout raise timed out again at exactly the old value.
|
|
187
|
+
Each `llm-relay mcp` process loads the file once too, so a host restart is needed for it as well.
|
|
188
|
+
`GET /telemetry` carries `config.changedOnDisk`, and `routing show|get`, `config show|get` and
|
|
189
|
+
`offload status` print a notice when the running relay has not loaded an edit. Restart: stop the
|
|
190
|
+
node process running `dist\cli.js` with no subcommand (`llm-relay stop` since v0.78.0), then
|
|
191
|
+
relaunch `wscript.exe "…\Startup\llm-relay.vbs"`; verify with `GET /telemetry` and
|
|
192
|
+
`llm-relay dispatch --tier high`.
|
|
193
|
+
- **A pool request to DeepSeek runs with thinking ON, which spends output tokens on reasoning
|
|
194
|
+
(2026-09-09, causes since addressed).** Two authorized paid calls to `deepseek/deepseek-v4-pro`
|
|
195
|
+
returned HTTP 200 and `max_tokens` with zero final text. llm-relay forwards the caller's thinking
|
|
196
|
+
control since 2026-09-10 and `dispatch` takes a `model` argument since v0.81.0. For a pool
|
|
197
|
+
request, give a large `max_tokens` or send `thinking: {"type": "disabled"}`.
|
|
198
|
+
- **Codex Desktop cannot reach a relay pool through a collaboration child (2026-08-31).** With a
|
|
199
|
+
ChatGPT account the launcher validates `pool/medium` against the parent account before contacting
|
|
200
|
+
llm-relay and fails with HTTP 400 `The 'pool/medium' model is not supported when using Codex with
|
|
201
|
+
a ChatGPT account.` Use the MCP `dispatch` tool there. Generated agent files stay valid for
|
|
202
|
+
clients that honour custom providers.
|