Skip to content
This repository was archived by the owner on Oct 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "model-router",
"version": "0.25.1",
"version": "0.26.0",
"description": "Route authorized software work through one durable, capacity-aware subscription policy.",
"author": {
"name": "toolboxmd",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "model-router",
"version": "0.25.1",
"version": "0.26.0",
"description": "Route authorized software work through one durable, capacity-aware subscription policy.",
"author": {
"name": "toolboxmd",
Expand Down
2 changes: 1 addition & 1 deletion .grok-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "model-router",
"version": "0.25.1",
"version": "0.26.0",
"description": "Route authorized software work through one durable, capacity-aware subscription policy.",
"author": {
"name": "toolboxmd",
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.26.0] - 2026-09-23

### Added

- Remove the manual-only Codex dispatch route and resume Codex on the job's active dispatch route

## [0.25.1] - 2026-09-23

### Changed
Expand Down
2 changes: 1 addition & 1 deletion RUNNER.md
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ test keeps the two equal; `policy.main(['validate'])` checks the data.
Stages in order: planning (Fable 5.1 max in Claude Code, then Astra max on
Codex; Sonnet medium only as the explicit live-test override), dispatch (Luna
max on Codex read-only first; `luna-go/max` on Go in OpenCode plan mode as the
recorded fallback; Terra max is a manual-only option), the implementation
recorded fallback), the implementation
lanes default, small, and hard, planner-chosen rungs (Astra medium on Codex,
Opus 5.5 high on the Claude harness; the planner itself chooses and runs them),
critical
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.25.1
0.26.0
11 changes: 5 additions & 6 deletions runner/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,8 @@ def _save_codex_task(state_dir, request_id: str, task_id: str,
effort: str = adapters.CODEX_EFFORT,
adapter: str = "codex",
dispatch_route: str | None = None) -> dict:
"""Persist the dispatcher task and, for the OpenCode fallback, its route
in one transaction, so a crash between two writes cannot leave a session
without its harness."""
"""Persist the dispatcher task and its active route in one transaction,
so a crash between two writes cannot leave a session without its harness."""
con = store.connect(state_dir)
try:
con.execute("BEGIN IMMEDIATE")
Expand Down Expand Up @@ -743,7 +742,7 @@ def dispatch(state_dir, request_id: str, run_cmd=None, probe=None) -> dict:
# The thread exists even when the turn failed: save it so recovery
# resumes it instead of creating a replacement task.
_save_codex_task(state_dir, request_id, task_id, model=dispatch["model"],
effort=dispatch["variant"])
effort=dispatch["variant"], dispatch_route=dispatch_route)
if not codex_h.turn_ok("codex_dispatch", rc, out, cmd):
# A missing or rejected login blocks with its reason: no other
# route holds the same login, so there is no fallback.
Expand Down Expand Up @@ -1008,12 +1007,12 @@ def resume_luna(state_dir, request_id: str, prompt: str, run_cmd=None,
reason="resume", session_id=task_id, phase="resumed")
last_path = _last_message_path(state_dir, request_id,
"codex-resume-" + _prompt_digest(message))
dispatch = policy.ROUTES[policy.stage_routes("dispatch")[0]]
dispatch = policy.ROUTES[active]
cmd = adapters.build_codex_resume_cmd(task_id, message, last_message_path=last_path,
model=dispatch["model"], effort=dispatch["variant"])
# Resume uses the saved workspace as cwd; never --cd/--reasoning.
rc, out, err = run_cmd(cmd, job["workspace"], None, kind="codex_resume",
meta={"stage": "dispatch", "route": "luna/max", "reason": "resume"})
meta={"stage": "dispatch", "route": active, "reason": "resume"})
codex_h = harnesses.harness_named("codex")
resumed_id, _skind = codex_h.parse_session("codex_resume", out, err, job)
_record_child(state_dir, request_id, "codex_resume", cmd, rc,
Expand Down
12 changes: 4 additions & 8 deletions runner/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
from pathlib import Path

POLICY_ID = "durable-runner-policy-v2"
POLICY_VERSION = "2.5.0"
POLICY_VERSION = "2.6.0"
# Provenance: who decided this policy and where the evidence lives.
POLICY_SOURCE = ("human decision, toolboxmd/model-router#10 (amended 2026-09-19), "
"#12, #26 (Go plan, 2026-09-20), #28/#29 (role kits, 2026-09-20), "
"#34 (usage probes, 2026-09-20), #38 (planner handoff, 2026-09-20), "
"and #17 (Opus 5.5, human request, 2026-09-23)")
"#17 (Opus 5.5, human request, 2026-09-23), "
"and #62 (manual-only dispatch route removed, human direction, 2026-09-23)")
POLICY_EVIDENCE = "https://github.com/toolboxmd/model-router/issues/29"

# Subscription pools only. No Zen balance overflow, no pay-per-token APIs.
Expand Down Expand Up @@ -189,10 +190,6 @@
"variant": "medium", "role": "planning", "family": "claude",
"override_only": True, "context_window": 200_000,
"note": "explicit live-test override only, never a default"},
"terra/max": {"harness": "codex", "pool": "codex", "model": "gpt-5.6-terra",
"variant": "max", "role": "dispatch", "family": "gpt",
"manual": True, "context_window": 400_000,
"note": "manual-only option; never selected automatically"},
"luna/max": {"harness": "codex", "pool": "codex", "model": "gpt-5.6-luna",
"variant": "max", "role": "dispatch", "family": "gpt",
"sandbox": "read-only", "context_window": 400_000},
Expand Down Expand Up @@ -309,10 +306,9 @@
"note": "Fable 5.1 in Claude Code, then Astra max on Codex; "
"Sonnet medium only as the explicit live-test override"},
"dispatch": {"executor": "runner", "routes": ["luna/max", "luna-go/max"],
"manual": ["terra/max"],
"capabilities": ["read_only", "session_resume", "structured_output"],
"note": "read-only Codex sandbox first; the same model on Go in OpenCode plan mode "
"when Codex cannot start the task; Terra max is a manual-only option"},
"when Codex cannot start the task"},
"implementation_default": {"executor": "runner",
"routes": ["muse-spark-xhigh-free", "muse-spark-xhigh-go",
"glm-5.3-flash-go", "qwen3.8-flash-go",
Expand Down
8 changes: 4 additions & 4 deletions skills/model-routing/references/codex.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Routing policy

Generated from `runner/policy.py` (`durable-runner-policy-v2` 2.5.0); do not edit by hand.
Source: human decision, toolboxmd/model-router#10 (amended 2026-09-19), #12, #26 (Go plan, 2026-09-20), #28/#29 (role kits, 2026-09-20), #34 (usage probes, 2026-09-20), #38 (planner handoff, 2026-09-20), and #17 (Opus 5.5, human request, 2026-09-23). Evidence: https://github.com/toolboxmd/model-router/issues/29.
Generated from `runner/policy.py` (`durable-runner-policy-v2` 2.6.0); do not edit by hand.
Source: human decision, toolboxmd/model-router#10 (amended 2026-09-19), #12, #26 (Go plan, 2026-09-20), #28/#29 (role kits, 2026-09-20), #34 (usage probes, 2026-09-20), #38 (planner handoff, 2026-09-20), #17 (Opus 5.5, human request, 2026-09-23), and #62 (manual-only dispatch route removed, human direction, 2026-09-23). Evidence: https://github.com/toolboxmd/model-router/issues/29.

| Stage | Routes in order | Notes |
| --- | --- | --- |
| planning | `fable-5.1/max`, `astra/max` (override: `sonnet/medium`) | host; Fable 5.1 in Claude Code, then Astra max on Codex; Sonnet medium only as the explicit live-test override |
| dispatch | `luna/max`, `luna-go/max` (manual-only: `terra/max`) | runner; read-only Codex sandbox first; the same model on Go in OpenCode plan mode when Codex cannot start the task; Terra max is a manual-only option |
| dispatch | `luna/max`, `luna-go/max` | runner; read-only Codex sandbox first; the same model on Go in OpenCode plan mode when Codex cannot start the task |
| implementation_default | `muse-spark-xhigh-free`, `muse-spark-xhigh-go`, `glm-5.3-flash-go`, `qwen3.8-flash-go`, `deepseek-v4.1-flash-go`, `hy3-go`, `minimax-m3-go`, `mimo-v2.5-go`, `minimax-m2.7-go`, `longcat-2.0-go`, `glm-5.2-go`, `kimi-k2.6-go`, `glm-5.1-go` | runner; the full Go implementer chain in intelligence order, every new job starting on Muse free (no concurrency cap, parallel sessions by design); Qwen 3.7 Plus and 3.6 Plus are absent: no known tier. Policy declares exhaustion moves the same model to the next pool where declared (free Muse to Go Muse, Go Grok to xAI Grok), otherwise to the next family; overload moves to the next model family within one minute |
| implementation_small | `muse-spark-xhigh-free`, `muse-spark-xhigh-go`, `glm-5.3-flash-go`, `qwen3.8-flash-go`, `deepseek-v4.1-flash-go`, `hy3-go`, `minimax-m3-go`, `mimo-v2.5-go`, `minimax-m2.7-go`, `longcat-2.0-go`, `glm-5.2-go`, `kimi-k2.6-go`, `glm-5.1-go` | runner; small bounded edits; every new job starts on Muse free (no concurrency cap, parallel sessions by design), then Muse Go, then from GLM 5.3 Flash onward |
| implementation_hard | `muse-spark-xhigh-free`, `muse-spark-xhigh-go`, `glm-5.3-go`, `deepseek-v4-pro-go`, `grok-4.6-go`, `grok-4.6-build`, `grok-4.6-xai` | runner; $15 models get one turn per job; every new job starts on Muse free (no cap); Grok 4.6 sits here as the hard lane's last Go rung, then native Grok Build on the xAI subscription, then OpenCode's xAI provider |
Expand All @@ -21,7 +21,7 @@ Source: human decision, toolboxmd/model-router#10 (amended 2026-09-19), #12, #26

The planner itself chooses one rung and runs it in its own session; the runner never selects these automatically: `astra/medium` = `gpt-6-astra` medium; `opus-5.5/high` = `claude-opus-5-5` high.

Route models, in policy order: `fable-5.1/max` = `claude-fable-5-1` max; `astra/max` = `gpt-6-astra` max; `sonnet/medium` = `claude-sonnet-5` medium; `terra/max` = `gpt-5.6-terra` max; `luna/max` = `gpt-5.6-luna` max; `luna-go/max` = `opencode-go/gpt-5.6-luna`; `luna-max-review` = `gpt-5.6-luna` max; `luna-go-review` = `opencode-go/gpt-5.6-luna`; `astra/high-review` = `gpt-6-astra` high; `opus-5.5/high-review` = `claude-opus-5-5` high; `muse-spark-xhigh-free` = `opencode/muse-spark-1.3-contributor-free` xhigh; `muse-spark-xhigh-go` = `opencode-go/muse-spark-1.3-contributor` xhigh; `glm-5.3-go` = `opencode-go/glm-5.3`; `deepseek-v4-pro-go` = `opencode-go/deepseek-v4-pro`; `kimi-k2.7-code-go` = `opencode-go/kimi-k2.7-code`; `glm-5.3-flash-go` = `opencode-go/glm-5.3-flash`; `qwen3.8-flash-go` = `opencode-go/qwen3.8-flash`; `deepseek-v4.1-flash-go` = `opencode-go/deepseek-v4.1-flash`; `hy3-go` = `opencode-go/hy3`; `minimax-m3-go` = `opencode-go/minimax-m3`; `mimo-v2.5-go` = `opencode-go/mimo-v2.5`; `minimax-m2.7-go` = `opencode-go/minimax-m2.7`; `longcat-2.0-go` = `opencode-go/longcat-2.0`; `glm-5.2-go` = `opencode-go/glm-5.2`; `kimi-k2.6-go` = `opencode-go/kimi-k2.6`; `glm-5.1-go` = `opencode-go/glm-5.1`; `astra/medium` = `gpt-6-astra` medium; `opus-5.5/high` = `claude-opus-5-5` high; `grok-4.6-go` = `opencode-go/grok-4.6` medium; `grok-4.6-build` = `grok-4.6` medium; `grok-4.6-xai` = `xai/grok-4.6` medium.
Route models, in policy order: `fable-5.1/max` = `claude-fable-5-1` max; `astra/max` = `gpt-6-astra` max; `sonnet/medium` = `claude-sonnet-5` medium; `luna/max` = `gpt-5.6-luna` max; `luna-go/max` = `opencode-go/gpt-5.6-luna`; `luna-max-review` = `gpt-5.6-luna` max; `luna-go-review` = `opencode-go/gpt-5.6-luna`; `astra/high-review` = `gpt-6-astra` high; `opus-5.5/high-review` = `claude-opus-5-5` high; `muse-spark-xhigh-free` = `opencode/muse-spark-1.3-contributor-free` xhigh; `muse-spark-xhigh-go` = `opencode-go/muse-spark-1.3-contributor` xhigh; `glm-5.3-go` = `opencode-go/glm-5.3`; `deepseek-v4-pro-go` = `opencode-go/deepseek-v4-pro`; `kimi-k2.7-code-go` = `opencode-go/kimi-k2.7-code`; `glm-5.3-flash-go` = `opencode-go/glm-5.3-flash`; `qwen3.8-flash-go` = `opencode-go/qwen3.8-flash`; `deepseek-v4.1-flash-go` = `opencode-go/deepseek-v4.1-flash`; `hy3-go` = `opencode-go/hy3`; `minimax-m3-go` = `opencode-go/minimax-m3`; `mimo-v2.5-go` = `opencode-go/mimo-v2.5`; `minimax-m2.7-go` = `opencode-go/minimax-m2.7`; `longcat-2.0-go` = `opencode-go/longcat-2.0`; `glm-5.2-go` = `opencode-go/glm-5.2`; `kimi-k2.6-go` = `opencode-go/kimi-k2.6`; `glm-5.1-go` = `opencode-go/glm-5.1`; `astra/medium` = `gpt-6-astra` medium; `opus-5.5/high` = `claude-opus-5-5` high; `grok-4.6-go` = `opencode-go/grok-4.6` medium; `grok-4.6-build` = `grok-4.6` medium; `grok-4.6-xai` = `xai/grok-4.6` medium.

Rules:

Expand Down
38 changes: 38 additions & 0 deletions tests/test_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import threading
import time
import unittest
from unittest import mock
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
Expand Down Expand Up @@ -259,6 +260,43 @@ def fake_resume(cmd, cwd=None, timeout=120, **kw):
# Never contains a different task ID.
self.assertNotIn("codex-other", " ".join(cmd))

def test_dispatch_persists_its_codex_route_and_resume_uses_it(self):
# A second Codex dispatch route listed first: dispatch must persist
# the route it actually ran on, and resume must use that route even
# after the stage order changes back.
alt = dict(policy.ROUTES["luna/max"], model="gpt-5.6-luna-alt")
orig_routes = list(policy.STAGES["dispatch"]["routes"])
captured = {}

def fake_dispatch(cmd, cwd=None, timeout=120, **kw):
captured["dispatch_cmd"] = list(cmd)
return 0, codex_out("codex-active-1", {"action": "planner_question",
"qid": "q1", "prompt": "Confirm?"}), ""

def fake_resume(cmd, cwd=None, timeout=120, **kw):
captured["cmd"] = list(cmd)
captured["meta"] = kw["meta"]
return 0, codex_out("codex-active-1", {"action": "completion", "output": "done"}), ""

with mock.patch.dict(policy.ROUTES, {"luna-alt/max": alt}):
policy.STAGES["dispatch"]["routes"] = ["luna-alt/max"] + orig_routes
try:
core.submit(self.sd, "r1", {"goal": "t"}, self.ws(), "claude-1")
controller.dispatch(self.sd, "r1", run_cmd=fake_dispatch,
probe=lambda *a: None)
finally:
policy.STAGES["dispatch"]["routes"] = orig_routes
job = core.get_job(self.sd, "r1")
self.assertEqual(controller._load_controller_state(dict(job))["dispatch_route"],
"luna-alt/max")
self.assertEqual(policy.stage_routes("dispatch")[0], "luna/max")
controller.resume_luna(self.sd, "r1", "follow up", run_cmd=fake_resume)
dispatch_cmd = captured["dispatch_cmd"]
self.assertEqual(dispatch_cmd[dispatch_cmd.index("--model") + 1], "gpt-5.6-luna-alt")
self.assertEqual(captured["cmd"][captured["cmd"].index("-m") + 1], "gpt-5.6-luna-alt")
self.assertIn('model_reasoning_effort="max"', captured["cmd"])
self.assertEqual(captured["meta"]["route"], "luna-alt/max")

def test_planner_resume_never_forks_new_session(self):
core.submit(self.sd, "r1", {"goal": "t"}, self.ws(), "claude-exact-77")
captured = {}
Expand Down
9 changes: 4 additions & 5 deletions tests/test_policy_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ def test_stage_table_matches_parent_decision(self):
self.assertEqual(s["correction"]["routes"], ["kimi-k2.7-code-go"])
self.assertEqual(s["recovery"]["routes"], ["grok-4.6-go", "grok-4.6-build", "grok-4.6-xai"])
self.assertEqual(s["dispatch"]["routes"], ["luna/max", "luna-go/max"])
self.assertEqual(s["dispatch"]["manual"], ["terra/max"])
self.assertTrue(policy.ROUTES["terra/max"]["manual"])
self.assertNotIn("manual", s["dispatch"])
self.assertEqual(policy.ROUTES["luna-go/max"]["agent"], "plan")
# Planner and review fallback routes on other subscriptions.
self.assertEqual(s["planning"]["routes"], ["fable-5.1/max", "astra/max"])
Expand All @@ -148,7 +147,7 @@ def test_planner_chosen_rungs_are_not_implementation_lanes(self):
self.assertFalse(policy.ROUTES[route].get("planner_chosen"), (stage, route))
self.assertFalse(policy.ROUTES[route].get("manual"), (stage, route))
for route in policy.implementation_routes() + policy.stage_routes("correction"):
self.assertNotEqual(policy.route_spec(route).get("variant"), "terra")
self.assertFalse(policy.route_spec(route).get("manual"))

def test_route_params_and_pool_moves(self):
self.assertEqual(policy.opencode_route_params("glm-5.3-flash-go"),
Expand Down Expand Up @@ -542,15 +541,15 @@ def test_max_concurrent_must_be_exactly_one(self):
def test_guarded_route_in_lane_recovery_or_dispatch_fails(self):
orig_routes = list(policy.STAGES["implementation_default"]["routes"])
try:
policy.STAGES["implementation_default"]["routes"] = orig_routes + ["terra/max"]
policy.STAGES["implementation_default"]["routes"] = orig_routes + ["astra/medium"]
problems = policy.validate_policy()
self.assertTrue(any("implementation_default" in p and "never" in p for p in problems),
problems)
finally:
policy.STAGES["implementation_default"]["routes"] = orig_routes
orig_rec = list(policy.STAGES["recovery"]["routes"])
try:
policy.STAGES["recovery"]["routes"] = orig_rec + ["terra/max"]
policy.STAGES["recovery"]["routes"] = orig_rec + ["astra/medium"]
problems = policy.validate_policy()
self.assertTrue(any("recovery" in p and "never" in p for p in problems), problems)
finally:
Expand Down
Loading