coderouter-cli 2.9.2__tar.gz → 2.9.3__tar.gz

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.
Files changed (273) hide show
  1. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/CHANGELOG.md +84 -0
  2. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/PKG-INFO +1 -1
  3. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/schemas.py +41 -6
  4. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/launcher_routes.py +60 -3
  5. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/launcher_swap.py +100 -12
  6. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/launcher.en.md +4 -2
  7. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/launcher.md +4 -2
  8. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/orchestration-companion.md +8 -7
  9. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/pyproject.toml +1 -1
  10. coderouter_cli-2.9.3/tests/test_launcher_gui_readiness.py +650 -0
  11. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_swap.py +277 -8
  12. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_swap_review.py +4 -2
  13. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/.gitignore +0 -0
  14. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/LICENSE +0 -0
  15. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/README.en.md +0 -0
  16. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/README.md +0 -0
  17. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/__init__.py +0 -0
  18. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/__main__.py +0 -0
  19. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/__init__.py +0 -0
  20. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/anthropic_native.py +0 -0
  21. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/base.py +0 -0
  22. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/openai_compat.py +0 -0
  23. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/adapters/registry.py +0 -0
  24. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/cli.py +0 -0
  25. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/cli_stats.py +0 -0
  26. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/__init__.py +0 -0
  27. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/capability_registry.py +0 -0
  28. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/env_file.py +0 -0
  29. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/config/loader.py +0 -0
  30. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/cost.py +0 -0
  31. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/data/__init__.py +0 -0
  32. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/data/model-capabilities.yaml +0 -0
  33. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/doctor.py +0 -0
  34. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/doctor_apply.py +0 -0
  35. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/env_security.py +0 -0
  36. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/errors.py +0 -0
  37. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/gguf_introspect.py +0 -0
  38. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/__init__.py +0 -0
  39. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/_fingerprint.py +0 -0
  40. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/backend_health.py +0 -0
  41. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/context_budget.py +0 -0
  42. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/continuous_probe.py +0 -0
  43. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/drift_actions.py +0 -0
  44. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/drift_detection.py +0 -0
  45. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/memory_budget.py +0 -0
  46. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/memory_pressure.py +0 -0
  47. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/self_healing.py +0 -0
  48. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/guards/tool_loop.py +0 -0
  49. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/hardware.py +0 -0
  50. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/__init__.py +0 -0
  51. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/anthropic_routes.py +0 -0
  52. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/app.py +0 -0
  53. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/dashboard_routes.py +0 -0
  54. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/metrics_routes.py +0 -0
  55. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/ingress/openai_routes.py +0 -0
  56. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/language_tax.py +0 -0
  57. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/launcher_speculative.py +0 -0
  58. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/logging.py +0 -0
  59. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/metrics/__init__.py +0 -0
  60. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/metrics/collector.py +0 -0
  61. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/metrics/prometheus.py +0 -0
  62. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/output_filters.py +0 -0
  63. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/plugins/__init__.py +0 -0
  64. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/plugins/base.py +0 -0
  65. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/plugins/loader.py +0 -0
  66. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/plugins/registry.py +0 -0
  67. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/__init__.py +0 -0
  68. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/adaptive.py +0 -0
  69. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/auto_router.py +0 -0
  70. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/budget.py +0 -0
  71. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/capability.py +0 -0
  72. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/routing/fallback.py +0 -0
  73. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/__init__.py +0 -0
  74. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/audit_log.py +0 -0
  75. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/replay.py +0 -0
  76. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/request_log.py +0 -0
  77. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/store.py +0 -0
  78. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/state/suggest_rules.py +0 -0
  79. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/token_estimation.py +0 -0
  80. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/token_estimation_accurate.py +0 -0
  81. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/translation/__init__.py +0 -0
  82. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/translation/anthropic.py +0 -0
  83. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/translation/convert.py +0 -0
  84. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/coderouter/translation/tool_repair.py +0 -0
  85. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/README.en.md +0 -0
  86. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/README.md +0 -0
  87. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/assets/dashboard-demo.png +0 -0
  88. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
  89. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  90. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/external-agents.en.md +0 -0
  91. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/external-agents.md +0 -0
  92. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/gguf_dl.en.md +0 -0
  93. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/gguf_dl.md +0 -0
  94. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/hf-ollama-models.en.md +0 -0
  95. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/hf-ollama-models.md +0 -0
  96. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/install-backends.en.md +0 -0
  97. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/install-backends.md +0 -0
  98. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/launcher-quickstart.en.md +0 -0
  99. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/launcher-quickstart.md +0 -0
  100. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/llamacpp-direct.en.md +0 -0
  101. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/llamacpp-direct.md +0 -0
  102. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/lmstudio-direct.en.md +0 -0
  103. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/lmstudio-direct.md +0 -0
  104. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/backends/verify-ollama-0.23.1.md +0 -0
  105. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/architecture.en.md +0 -0
  106. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/architecture.md +0 -0
  107. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/context-budget.en.md +0 -0
  108. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/context-budget.md +0 -0
  109. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/continuous-probing.en.md +0 -0
  110. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/continuous-probing.md +0 -0
  111. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/drift-detection.en.md +0 -0
  112. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/drift-detection.md +0 -0
  113. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/partial-stitch.en.md +0 -0
  114. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/concepts/partial-stitch.md +0 -0
  115. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/agent-cli-plugin-extraction.md +0 -0
  116. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/external-agents-adapter.md +0 -0
  117. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/launcher-model-swap.md +0 -0
  118. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  119. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/v1.6-auto-router-verification.md +0 -0
  120. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/designs/v1.6-auto-router.md +0 -0
  121. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/free-tier-guide.en.md +0 -0
  122. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/free-tier-guide.md +0 -0
  123. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/language-tax.en.md +0 -0
  124. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/language-tax.md +0 -0
  125. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/remote-access.en.md +0 -0
  126. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/remote-access.md +0 -0
  127. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/security.en.md +0 -0
  128. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/security.md +0 -0
  129. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/subagent-routing.en.md +0 -0
  130. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/subagent-routing.md +0 -0
  131. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/troubleshooting.en.md +0 -0
  132. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/troubleshooting.md +0 -0
  133. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/usage-guide.en.md +0 -0
  134. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/guides/usage-guide.md +0 -0
  135. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/low-memory-integration.en.md +0 -0
  136. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/low-memory-integration.md +0 -0
  137. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/openrouter-roster/CHANGES.md +0 -0
  138. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/openrouter-roster/README.md +0 -0
  139. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/openrouter-roster/latest.json +0 -0
  140. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.4.md +0 -0
  141. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.5-verify.md +0 -0
  142. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.5.md +0 -0
  143. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.6.md +0 -0
  144. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v0.7.md +0 -0
  145. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v1.0-verify.md +0 -0
  146. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/retrospectives/v1.0.md +0 -0
  147. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/start/quickstart.en.md +0 -0
  148. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/start/quickstart.md +0 -0
  149. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  150. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/docs/start/when-do-i-need-coderouter.md +0 -0
  151. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/.env.example +0 -0
  152. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/README.md +0 -0
  153. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers-agent-cli.yaml +0 -0
  154. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers-multiagent.yaml +0 -0
  155. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.context-budget-test.yaml +0 -0
  156. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.llamacpp-vllm.yaml +0 -0
  157. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.nvidia-nim.yaml +0 -0
  158. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.ollama-auto-custom.yaml +0 -0
  159. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.ollama-auto.yaml +0 -0
  160. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.ollama-free-chain.yaml +0 -0
  161. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.raspberrypi.yaml +0 -0
  162. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.swap.yaml +0 -0
  163. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/examples/providers.yaml +0 -0
  164. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/demo_traffic.sh +0 -0
  165. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/openrouter_roster_diff.py +0 -0
  166. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/smoke_v2_2.sh +0 -0
  167. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/verify-providers.yaml +0 -0
  168. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/verify_ollama_0_23.py +0 -0
  169. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/verify_v0_5.sh +0 -0
  170. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/scripts/verify_v1_0.sh +0 -0
  171. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/__init__.py +0 -0
  172. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/conftest.py +0 -0
  173. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_adapter_anthropic.py +0 -0
  174. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_agent_cli.py +0 -0
  175. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_audit_log.py +0 -0
  176. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_auto_router.py +0 -0
  177. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_auto_router_cjk.py +0 -0
  178. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_backend_health.py +0 -0
  179. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_budget.py +0 -0
  180. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability.py +0 -0
  181. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability_degraded_payload.py +0 -0
  182. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability_registry.py +0 -0
  183. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability_registry_cache_control.py +0 -0
  184. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_capability_tool_choice.py +0 -0
  185. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_claude_code_suitability.py +0 -0
  186. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_cli.py +0 -0
  187. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_cli_bind_warning.py +0 -0
  188. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_cli_stats.py +0 -0
  189. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_config.py +0 -0
  190. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_config_shim_actions.py +0 -0
  191. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_context_budget.py +0 -0
  192. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_continuous_probe.py +0 -0
  193. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_dashboard_endpoint.py +0 -0
  194. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_doctor.py +0 -0
  195. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_doctor_apply.py +0 -0
  196. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_doctor_cache_probe.py +0 -0
  197. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_drift_actions.py +0 -0
  198. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_drift_detection.py +0 -0
  199. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_drift_detection_integration.py +0 -0
  200. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_env_file.py +0 -0
  201. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_env_security.py +0 -0
  202. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_errors.py +0 -0
  203. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_examples_yaml.py +0 -0
  204. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback.py +0 -0
  205. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_anthropic.py +0 -0
  206. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_cache_control.py +0 -0
  207. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_cache_observed.py +0 -0
  208. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_cache_strip.py +0 -0
  209. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_empty_response.py +0 -0
  210. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_misconfig_warn.py +0 -0
  211. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_paid_gate.py +0 -0
  212. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_thinking.py +0 -0
  213. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fallback_tool_choice_emulate.py +0 -0
  214. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h1_prometheus_drift.py +0 -0
  215. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h2_h6_translation.py +0 -0
  216. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h3_client_reuse.py +0 -0
  217. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h4_h5_guards.py +0 -0
  218. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h7_probe_restore.py +0 -0
  219. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_h8_launcher_auth.py +0 -0
  220. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mA_routing.py +0 -0
  221. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mB_adapters.py +0 -0
  222. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mC_convert.py +0 -0
  223. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mD_logging_metrics.py +0 -0
  224. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mE_config_validation.py +0 -0
  225. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_fix_mF_ingress.py +0 -0
  226. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_gguf_introspect.py +0 -0
  227. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_guards_tool_loop.py +0 -0
  228. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_hardware.py +0 -0
  229. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_ingress_anthropic.py +0 -0
  230. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_ingress_count_tokens.py +0 -0
  231. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_ingress_profile.py +0 -0
  232. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_language_tax.py +0 -0
  233. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_language_tax_integration.py +0 -0
  234. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_mtp.py +0 -0
  235. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_mtp_fallback.py +0 -0
  236. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_provider_sync.py +0 -0
  237. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_launcher_readiness_restart.py +0 -0
  238. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_memory_budget.py +0 -0
  239. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_memory_pressure.py +0 -0
  240. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_cache.py +0 -0
  241. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_collector.py +0 -0
  242. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_cost.py +0 -0
  243. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_endpoint.py +0 -0
  244. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_jsonl.py +0 -0
  245. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_prometheus.py +0 -0
  246. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_metrics_prometheus_cache.py +0 -0
  247. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_models_passthrough.py +0 -0
  248. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_openai_compat.py +0 -0
  249. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_openrouter_roster_diff.py +0 -0
  250. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_output_filters.py +0 -0
  251. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_output_filters_adapters.py +0 -0
  252. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_partial_stitch.py +0 -0
  253. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_plugin_adapter.py +0 -0
  254. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_plugins_integration.py +0 -0
  255. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_plugins_loader.py +0 -0
  256. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_plugins_registry.py +0 -0
  257. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_reasoning_strip.py +0 -0
  258. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_repair_byte_fallback.py +0 -0
  259. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_request_log.py +0 -0
  260. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_role_normalization.py +0 -0
  261. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_routing_adaptive.py +0 -0
  262. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_self_healing.py +0 -0
  263. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_setup_sh.py +0 -0
  264. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_state_store.py +0 -0
  265. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_token_estimation.py +0 -0
  266. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_token_estimation_accurate.py +0 -0
  267. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_tokens_saved_metric.py +0 -0
  268. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_tool_repair.py +0 -0
  269. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_tool_repair_lenient.py +0 -0
  270. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_tool_repair_r4.py +0 -0
  271. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_toolrepair_bench.py +0 -0
  272. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_translation_anthropic.py +0 -0
  273. {coderouter_cli-2.9.2 → coderouter_cli-2.9.3}/tests/test_translation_reverse.py +0 -0
@@ -9,6 +9,90 @@ are kept verbatim where the Japanese text itself is the subject).
9
9
 
10
10
  ---
11
11
 
12
+ ## [v2.9.3] — 2026-07-12 (GUI/Web parity, swap polish, launcher UI fixes)
13
+
14
+ ### Added
15
+
16
+ - **Per-model TTL override for `launcher.swap`.** `SwapModelSpec.ttl_seconds`
17
+ (new, optional, `None` by default) lets a catalog entry override the
18
+ global `launcher.swap.ttl_seconds` for just that one model — `None`
19
+ keeps following the global value, `0` unloads the model as soon as
20
+ its last in-flight lease releases (same meaning as the global field's
21
+ `0`, just scoped to one entry). `SwapManager.sweep_once` now resolves
22
+ each model's effective TTL individually, and the sweeper starts
23
+ whenever either the global TTL or at least one catalog entry's
24
+ override is set (previously gated solely on the global value). See
25
+ `tests/test_launcher_swap.py` (`test_ttl_override_spec_wins_over_global`,
26
+ `test_ttl_override_unset_falls_back_to_global`,
27
+ `test_ttl_override_zero_unloads_even_when_global_ttl_disabled`) and the
28
+ updated `launcher.swap` field table in `docs/backends/launcher.md` /
29
+ `launcher.en.md`.
30
+ - **Configurable ephemeral-port retry count for `launcher.swap`.**
31
+ `LauncherSwapConfig.port_retry_attempts` (new, default `2`, range
32
+ 0–5) replaces the previously hard-coded single retry for catalog
33
+ entries that leave `port` unset — the swap manager now attempts up
34
+ to `1 + port_retry_attempts` spawns (each on a freshly picked
35
+ ephemeral port) before giving up, versus the previous fixed 2 total
36
+ attempts. Fixed-port entries are unaffected (still exactly one
37
+ attempt — a second try on the same port would just collide again).
38
+ This narrows the practical impact of the known pick-then-bind TOCTOU
39
+ window documented on `SwapModelSpec.port` / `_pick_ephemeral_port`
40
+ but does not close it outright; a fixed `port` remains the only way
41
+ to eliminate the race entirely. See
42
+ `tests/test_launcher_swap.py`
43
+ (`test_port_none_retries_default_port_retry_attempts`,
44
+ `test_port_retry_attempts_exhausted_raises`,
45
+ `test_port_retry_attempts_configurable_to_zero`).
46
+ - **`/launcher` UI now shows which processes are swap-managed.**
47
+ `GET /api/launcher/processes` includes `swap_managed` (bool) and
48
+ `swap_model` (the swap catalog model name, or `null`) for every
49
+ process; `spawn_process` gained a `swap_model` kwarg that
50
+ `SwapManager._spawn` now passes through. The process table renders a
51
+ small "swap" badge next to the name of any swap-managed process
52
+ (title shows the backing catalog model when known). Manually-started
53
+ processes are unaffected (`swap_managed: false`, `swap_model: null`).
54
+ See `tests/test_launcher_swap.py::test_i1_on_demand_spawn_reaches_200`.
55
+
56
+ ### Fixed
57
+
58
+ - **Swap-managed "stopped" processes no longer accumulate in the launcher
59
+ registry.** `stop_process` only sets `status="stopped"` and never
60
+ removes the entry — deliberate for manually started processes (the
61
+ stopped row is visible history in the /launcher UI, with logs and an
62
+ explicit ✕ delete button), but swap-managed processes went through the
63
+ same path, so every failed readiness attempt left one permanent
64
+ "stopped" row (`1 + port_retry_attempts` rows per failed load — 3 with
65
+ the defaults) and every TTL unload left another, all growing
66
+ `GET /api/launcher/processes` and the UI without bound. `SwapManager`
67
+ now removes its own processes from the registry after stopping them,
68
+ in both the failed-readiness cleanup (`_spawn_with_retry`) and the TTL
69
+ unload (`_unload_locked`), guarded on `ManagedProcess.swap_managed` so
70
+ a manual process is never swept up. Crash leftovers (a swap process
71
+ that died on its own) are deliberately kept — their log tail is the
72
+ only crash forensics an operator has. See `tests/test_launcher_swap.py`
73
+ (`test_failed_load_leaves_no_registry_litter`,
74
+ `test_ttl_unload_removes_registry_entry`,
75
+ `test_registry_removal_skips_non_swap_processes`; adapted from the
76
+ review repro with its assertions inverted).
77
+ - **/launcher UI no longer 404-polls a removed process's logs forever.**
78
+ The log panel's poller (`poll()` → `refreshLogs()`, every 3s) never
79
+ checked the response status: once the polled id left the registry —
80
+ a `coderouter serve` restart under a still-open browser tab, a delete
81
+ issued from another client, or (with the fix above) a swap TTL unload
82
+ removing the entry mid-view — the 404 body has no `.logs`, the
83
+ resulting TypeError landed in the generic catch, and `selectedLogId`
84
+ was never cleared, so the tab kept hitting
85
+ `GET /api/launcher/logs/<stale-id>` and spamming `404 Not Found` into
86
+ the serve log indefinitely. The UI now stops polling as soon as the
87
+ selected id disappears from the periodic `/api/launcher/processes`
88
+ refresh (usually before a single 404 is even issued), treats a logs
89
+ 404 as terminal (shows "(process removed)" instead of retrying), and
90
+ the server keeps answering 404 for unknown ids — that behavior is
91
+ correct and now pinned by
92
+ `tests/test_launcher_swap.py::test_logs_unknown_proc_id_is_404`.
93
+
94
+ ---
95
+
12
96
  ## [v2.9.2] — 2026-07-12 (config: no more dummy provider for swap-only setups)
13
97
 
14
98
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.9.2
3
+ Version: 2.9.3
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -1518,10 +1518,25 @@ class SwapModelSpec(BaseModel):
1518
1518
  le=65535,
1519
1519
  description=(
1520
1520
  "Fixed port (recommended — §10 Q2). When unset, the swap "
1521
- "manager picks an OS-assigned ephemeral port and retries "
1522
- "once on a different port if the backend fails to become "
1523
- "ready. Best-effort only — no strong TOCTOU guarantee "
1524
- "(§6.6 known-trap #4)."
1521
+ "manager picks an OS-assigned ephemeral port and retries, "
1522
+ "each time on a freshly picked port, up to "
1523
+ "``LauncherSwapConfig.port_retry_attempts`` additional "
1524
+ "times if the backend fails to become ready. Best-effort "
1525
+ "only — no strong TOCTOU guarantee (§6.6 known-trap #4)."
1526
+ ),
1527
+ )
1528
+ ttl_seconds: float | None = Field(
1529
+ default=None,
1530
+ ge=0.0,
1531
+ description=(
1532
+ "[Unreleased] Per-model override of "
1533
+ "``LauncherSwapConfig.ttl_seconds``. ``None`` (default) = "
1534
+ "use the global value. ``0`` = unload as soon as the last "
1535
+ "in-flight lease for THIS model releases — same meaning as "
1536
+ "the global field's ``0``, just scoped to one catalog "
1537
+ "entry. Lets a large/expensive model unload sooner (or "
1538
+ "stay resident longer) than the rest of the catalog "
1539
+ "without changing the global default."
1525
1540
  ),
1526
1541
  )
1527
1542
  option_profile: str | None = Field(
@@ -1611,8 +1626,10 @@ class LauncherSwapConfig(BaseModel):
1611
1626
  "Seconds of no in-flight requests after which an idle swap "
1612
1627
  "process is auto-stopped. None = TTL disabled (runs until "
1613
1628
  "explicitly stopped). 0 = unload as soon as the last "
1614
- "in-flight lease releases. §10 Q1: one GLOBAL value for "
1615
- "Phase 1 no per-model override yet."
1629
+ "in-flight lease releases. §10 Q1: this is the GLOBAL "
1630
+ "default; a catalog entry's ``SwapModelSpec.ttl_seconds`` "
1631
+ "overrides it for that one model when set "
1632
+ "([Unreleased] per-model TTL override)."
1616
1633
  ),
1617
1634
  )
1618
1635
  readiness_timeout_s: float = Field(
@@ -1631,6 +1648,24 @@ class LauncherSwapConfig(BaseModel):
1631
1648
  le=600.0,
1632
1649
  description="How often the TTL sweeper background task scans for idle processes.",
1633
1650
  )
1651
+ port_retry_attempts: int = Field(
1652
+ default=2,
1653
+ ge=0,
1654
+ le=5,
1655
+ description=(
1656
+ "[Unreleased] Number of ADDITIONAL spawn attempts (each on a "
1657
+ "freshly picked ephemeral port) after the first, used only "
1658
+ "when a catalog entry's ``port`` is unset and the previous "
1659
+ "attempt fails to become ready. 0 = no retry. Ignored when "
1660
+ "``port`` is set — a fixed port never retries (a second "
1661
+ "attempt would just collide on the same port again). This "
1662
+ "does not close the pick-then-bind TOCTOU window (see "
1663
+ "``coderouter.launcher_swap._pick_ephemeral_port``); it only "
1664
+ "bounds how many times the swap manager re-rolls the dice. "
1665
+ "A fixed ``port`` remains the recommended way to eliminate "
1666
+ "the race entirely (§10 Q2)."
1667
+ ),
1668
+ )
1634
1669
  inject_auto_router_rules: bool = Field(
1635
1670
  default=True,
1636
1671
  description=(
@@ -171,6 +171,13 @@ class ManagedProcess:
171
171
  # under its per-model lock); a launcher auto-restart racing that
172
172
  # re-spawn would fight over the same fixed port.
173
173
  swap_managed: bool = False
174
+ # [Unreleased]: the swap catalog model name (SwapModelSpec.name) this
175
+ # process backs, set by SwapManager._spawn via spawn_process's
176
+ # swap_model kwarg. None for a manually-started process (swap_managed
177
+ # is False) or if a future swap-managed spawn path omits it. Purely
178
+ # informational — surfaced by GET /api/launcher/processes so the
179
+ # /launcher UI can label which catalog entry a running process is.
180
+ swap_model: str | None = None
174
181
  # asyncio subprocess handle — not serialised
175
182
  _proc: Any = field(default=None, repr=False, compare=False)
176
183
 
@@ -1027,7 +1034,13 @@ async def api_launcher_config_debug(request: Request) -> dict[str, Any]:
1027
1034
 
1028
1035
  @router.get("/api/launcher/processes")
1029
1036
  async def api_processes(request: Request) -> dict[str, Any]:
1030
- """List all managed processes."""
1037
+ """List all managed processes.
1038
+
1039
+ [Unreleased]: ``swap_managed`` / ``swap_model`` let the ``/launcher``
1040
+ UI (and other API clients) tell an on-demand SwapManager-spawned
1041
+ process apart from a manually-started one, and label which swap
1042
+ catalog model it backs.
1043
+ """
1031
1044
  reg = _registry(request)
1032
1045
  return {
1033
1046
  "processes": [
@@ -1040,6 +1053,8 @@ async def api_processes(request: Request) -> dict[str, Any]:
1040
1053
  "status": p.status,
1041
1054
  "pid": p.pid,
1042
1055
  "returncode": p.returncode,
1056
+ "swap_managed": p.swap_managed,
1057
+ "swap_model": p.swap_model,
1043
1058
  }
1044
1059
  for p in reg.all()
1045
1060
  ]
@@ -1100,6 +1115,7 @@ async def spawn_process(
1100
1115
  draft_model_path: str | None = None,
1101
1116
  mtp_mode: str = "auto",
1102
1117
  swap_managed: bool = False,
1118
+ swap_model: str | None = None,
1103
1119
  ) -> ManagedProcess:
1104
1120
  """Build argv, spawn the child process, and arm readiness/log tailing.
1105
1121
 
@@ -1116,6 +1132,12 @@ async def spawn_process(
1116
1132
  exist. Any other exception from ``create_subprocess_exec`` propagates
1117
1133
  as-is. Callers decide how to surface these (HTTP 400/500 for
1118
1134
  ``api_start``, a retryable ``AdapterError`` for SwapManager).
1135
+
1136
+ ``swap_model`` ([Unreleased]) is purely informational — the swap
1137
+ catalog model name (``SwapModelSpec.name``), recorded on the
1138
+ resulting ``ManagedProcess`` for ``GET /api/launcher/processes`` /
1139
+ the ``/launcher`` UI. Only ``SwapManager._spawn`` passes it; the
1140
+ manual ``POST /api/launcher/start`` path leaves it ``None``.
1119
1141
  """
1120
1142
  options = options if options is not None else {}
1121
1143
  configured_binary: str | None = None
@@ -1178,6 +1200,7 @@ async def spawn_process(
1178
1200
  spec_auto=spec_auto and fallback_cmd is not None,
1179
1201
  fallback_cmd=fallback_cmd,
1180
1202
  swap_managed=swap_managed,
1203
+ swap_model=swap_model,
1181
1204
  )
1182
1205
  proc.log_tail.append(f"[launcher] cmd: {' '.join(cmd)}")
1183
1206
  for note in spec_notes:
@@ -1378,6 +1401,10 @@ _LAUNCHER_HTML = r"""<!doctype html>
1378
1401
  <style>
1379
1402
  .dot { width:.5rem;height:.5rem;border-radius:9999px;display:inline-block; }
1380
1403
  .tabnum { font-variant-numeric:tabular-nums; }
1404
+ .badge-swap { font-size:10px; text-transform:uppercase; letter-spacing:.02em;
1405
+ background:rgba(99,102,241,.2); color:#a5b4fc; padding:1px 6px;
1406
+ border-radius:9999px; margin-left:6px; vertical-align:middle;
1407
+ white-space:nowrap; }
1381
1408
  .log-box { font-family:monospace;font-size:.75rem;line-height:1.4;
1382
1409
  overflow-y:auto;max-height:14rem;white-space:pre-wrap;word-break:break-all; }
1383
1410
  .model-row:hover { background:rgba(255,255,255,.04);cursor:pointer; }
@@ -1851,7 +1878,17 @@ _LAUNCHER_HTML = r"""<!doctype html>
1851
1878
  try {
1852
1879
  const r = await fetch("/api/launcher/processes");
1853
1880
  const d = await r.json();
1854
- renderProcesses(d.processes || []);
1881
+ const procs = d.processes || [];
1882
+ renderProcesses(procs);
1883
+ // [Unreleased] 404-loop fix: if the process whose logs we're
1884
+ // polling is no longer in the registry (deleted from another
1885
+ // client, swap TTL unload removed it, or the server restarted
1886
+ // under a still-open tab), stop the log polling BEFORE it even
1887
+ // issues a 404 — otherwise poll() would hit
1888
+ // /api/launcher/logs/<stale-id> every POLL_MS forever.
1889
+ if (selectedLogId && !procs.some(p => p.id === selectedLogId)) {
1890
+ markLogGone();
1891
+ }
1855
1892
  } catch (_) {}
1856
1893
  };
1857
1894
 
@@ -1871,8 +1908,14 @@ _LAUNCHER_HTML = r"""<!doctype html>
1871
1908
  ? `<button onclick="deleteProc('${p.id}')" class="btn-sm btn-slate ml-1">✕</button>`
1872
1909
  : "";
1873
1910
  const logBtn = `<button onclick="openLog('${p.id}','${esc(p.name)}')" class="btn-sm btn-indigo ml-1">📋 ログ</button>`;
1911
+ // [Unreleased]: swap badge — marks processes SwapManager spawned
1912
+ // on demand, distinct from manually-started ones. Title shows the
1913
+ // catalog model name when the backend supplied it.
1914
+ const swapBadge = p.swap_managed
1915
+ ? `<span class="badge-swap" title="${esc(p.swap_model ? "swap model: " + p.swap_model : "swap-managed")}">swap</span>`
1916
+ : "";
1874
1917
  return `<tr>
1875
- <td class="py-2 pr-3 font-medium">${esc(p.name)}</td>
1918
+ <td class="py-2 pr-3 font-medium">${esc(p.name)}${swapBadge}</td>
1876
1919
  <td class="py-2 pr-3 text-slate-400">${esc(p.backend)}</td>
1877
1920
  <td class="py-2 pr-3 text-slate-400 truncate max-w-[10rem]" title="${esc(p.model_path)}">${esc(modelName)}</td>
1878
1921
  <td class="py-2 pr-3 text-right">${p.port}</td>
@@ -1912,6 +1955,11 @@ _LAUNCHER_HTML = r"""<!doctype html>
1912
1955
  if (!selectedLogId) return;
1913
1956
  try {
1914
1957
  const r = await fetch(`/api/launcher/logs/${selectedLogId}?n=200`);
1958
+ // [Unreleased] 404-loop fix: the id is gone from the registry —
1959
+ // stop polling instead of retrying (and 404-spamming the serve
1960
+ // log) every POLL_MS forever. The server keeps answering 404 for
1961
+ // unknown ids (correct); the client just has to take the hint.
1962
+ if (r.status === 404) { markLogGone(); return; }
1915
1963
  const d = await r.json();
1916
1964
  const box = document.getElementById("log-box");
1917
1965
  box.textContent = d.logs.join("\n") || "(ログなし)";
@@ -1921,6 +1969,15 @@ _LAUNCHER_HTML = r"""<!doctype html>
1921
1969
  }
1922
1970
  };
1923
1971
 
1972
+ // [Unreleased] 404-loop fix: the process we were tailing no longer
1973
+ // exists server-side. Clear selectedLogId (which is what stops both
1974
+ // poll()'s refreshLogs call and any manual ↻) but keep the panel open
1975
+ // with a terminal message so the user sees WHY the logs stopped.
1976
+ const markLogGone = () => {
1977
+ selectedLogId = null;
1978
+ document.getElementById("log-box").textContent = "(process removed)";
1979
+ };
1980
+
1924
1981
  window.closeLog = () => {
1925
1982
  selectedLogId = null;
1926
1983
  document.getElementById("log-panel").classList.add("hidden");
@@ -85,9 +85,18 @@ class _ModelState:
85
85
  def _pick_ephemeral_port() -> int:
86
86
  """Ask the OS for a free port. Best-effort only (§6.6 known-trap #4):
87
87
 
88
- there is an unavoidable TOCTOU window between this call returning
89
- and the child actually binding the port. Fixed ``port:`` in the
90
- catalog entry avoids the race entirely (§10 Q2, recommended).
88
+ there is an unavoidable TOCTOU window between this call's bind+close
89
+ returning a "free" port and the child actually binding that same
90
+ port a moment later nothing stops another process (or another
91
+ concurrent swap spawn) from grabbing it first. Retrying on a fresh
92
+ port when the child fails to come up
93
+ (``LauncherSwapConfig.port_retry_attempts``, default 2 additional
94
+ attempts) narrows the *impact* of losing that race but does NOT
95
+ close the window itself — a genuinely race-free allocation would
96
+ need the OS to hand the child an already-bound socket fd, which the
97
+ current spawn path (argv ``--port N``) does not support. Fixed
98
+ ``port:`` in the catalog entry avoids the race entirely (§10 Q2,
99
+ recommended) and remains the only way to eliminate it outright.
91
100
  """
92
101
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
93
102
  s.bind(("127.0.0.1", 0))
@@ -126,6 +135,13 @@ class SwapManager:
126
135
  self._readiness_timeout_s = config.readiness_timeout_s
127
136
  self._sweep_interval_s = config.sweep_interval_s
128
137
  self._sweeper_task: asyncio.Task[None] | None = None
138
+ # [Unreleased] per-model TTL override: the sweeper must run
139
+ # whenever EITHER the global TTL is enabled OR at least one
140
+ # catalog entry sets its own ``ttl_seconds`` (even if the global
141
+ # value is None/disabled) — see _effective_ttl / start().
142
+ self._any_ttl_configured = self._ttl_seconds is not None or any(
143
+ m.ttl_seconds is not None for m in config.models
144
+ )
129
145
 
130
146
  # ------------------------------------------------------------------
131
147
  # Catalog matching
@@ -214,24 +230,40 @@ class SwapManager:
214
230
  async with state.lock:
215
231
  await self._unload_locked(state, spec, reason)
216
232
 
233
+ def _effective_ttl(self, spec: SwapModelSpec) -> float | None:
234
+ """``spec.ttl_seconds`` if set, else the global ``self._ttl_seconds``.
235
+
236
+ [Unreleased] per-model TTL override (docs/backends/launcher.md
237
+ "launcher.swap fields" table). ``None`` at either level keeps
238
+ its usual meaning ("TTL disabled") — a spec-level override only
239
+ takes effect when it's not ``None``.
240
+ """
241
+ return spec.ttl_seconds if spec.ttl_seconds is not None else self._ttl_seconds
242
+
217
243
  async def sweep_once(self) -> None:
218
244
  """One TTL sweep pass: stop every idle, un-leased, expired model.
219
245
 
220
246
  Takes each model's own lock independently (never more than one
221
247
  per-model lock at a time — §6.6 known-trap #7) so a slow unload
222
- for model A never delays the sweep of model B.
248
+ for model A never delays the sweep of model B. Each model's TTL
249
+ is resolved individually via :meth:`_effective_ttl` — a model
250
+ whose global TTL is disabled can still expire on its own
251
+ override, and vice versa.
223
252
  """
224
- if self._ttl_seconds is None:
253
+ if not self._any_ttl_configured:
225
254
  return
226
255
  now = time.monotonic()
227
256
  for spec in list(self._catalog.values()):
257
+ ttl = self._effective_ttl(spec)
258
+ if ttl is None:
259
+ continue
228
260
  state = self._states.get(spec.name)
229
261
  if state is None:
230
262
  continue
231
263
  async with state.lock:
232
264
  if state.status != "ready" or state.in_flight > 0:
233
265
  continue
234
- if (now - state.last_used) < self._ttl_seconds:
266
+ if (now - state.last_used) < ttl:
235
267
  continue
236
268
  await self._unload_locked(state, spec, reason="ttl")
237
269
 
@@ -240,8 +272,14 @@ class SwapManager:
240
272
  # ------------------------------------------------------------------
241
273
 
242
274
  async def start(self) -> None:
243
- """Start the TTL sweeper background task. No-op when TTL is disabled."""
244
- if self._ttl_seconds is None or self._sweeper_task is not None:
275
+ """Start the TTL sweeper background task.
276
+
277
+ No-op when TTL is disabled both globally AND on every catalog
278
+ entry (``self._any_ttl_configured`` — [Unreleased] per-model
279
+ override means a per-model ``ttl_seconds`` alone is enough to
280
+ need the sweeper, even with the global TTL off).
281
+ """
282
+ if not self._any_ttl_configured or self._sweeper_task is not None:
245
283
  return
246
284
  self._sweeper_task = asyncio.create_task(self._sweeper_loop())
247
285
 
@@ -285,6 +323,35 @@ class SwapManager:
285
323
  def _engine(self) -> Any | None:
286
324
  return getattr(self._app.state, "engine", None)
287
325
 
326
+ def _remove_from_registry(self, proc_id: str) -> None:
327
+ """Drop a swap-managed ``ManagedProcess`` from the launcher registry.
328
+
329
+ [Unreleased] registry-litter fix: ``stop_process`` only sets
330
+ ``status="stopped"`` — it never removes the entry, because for
331
+ MANUALLY started processes the stopped row doubles as visible
332
+ history in the /launcher UI (with logs and an explicit ✕ delete
333
+ button). Swap-managed processes have no such contract: every
334
+ failed readiness attempt (times ``1 + port_retry_attempts``) and
335
+ every TTL unload would otherwise leave a permanent "stopped" row
336
+ accumulating unboundedly in ``GET /api/launcher/processes``. So
337
+ SwapManager removes ITS OWN processes right after it stops them,
338
+ guarded on ``swap_managed`` so a manual process can never be
339
+ swept up by mistake. Crash leftovers (a swap process that died
340
+ on its own, status "error"/"stopped" without SwapManager
341
+ stopping it) are deliberately NOT removed here — their log tail
342
+ is the only crash forensics an operator has; the ✕ button still
343
+ applies.
344
+ """
345
+ reg = _registry_for_app(self._app)
346
+ try:
347
+ proc = reg.get(proc_id)
348
+ except KeyError:
349
+ return
350
+ if not proc.swap_managed:
351
+ return
352
+ with contextlib.suppress(KeyError):
353
+ reg.remove(proc_id)
354
+
288
355
  def _resolve_options(self, spec: SwapModelSpec) -> dict[str, Any]:
289
356
  """Resolve ``spec.option_profile`` into the launcher options dict."""
290
357
  if not spec.option_profile or self._launcher_cfg is None:
@@ -346,11 +413,20 @@ class SwapManager:
346
413
  async def _spawn_with_retry(
347
414
  self, spec: SwapModelSpec
348
415
  ) -> tuple[ManagedProcess | None, Exception | None]:
349
- """Spawn + wait for readiness. §10 Q2: one retry on a fresh ephemeral
350
- port when ``spec.port`` is unset and the first attempt doesn't come
351
- up (fixed ports never retry a second attempt would collide again).
416
+ """Spawn + wait for readiness.
417
+
418
+ §10 Q2: when ``spec.port`` is unset, up to
419
+ ``1 + LauncherSwapConfig.port_retry_attempts`` attempts total
420
+ (default: 1 initial + 2 retries = 3), each on a freshly picked
421
+ ephemeral port via :func:`_pick_ephemeral_port` — see that
422
+ function's docstring for the residual pick-then-bind TOCTOU
423
+ window this does NOT close, only bounds the impact of. Fixed
424
+ ports never retry — a second attempt on the same port would
425
+ just collide again.
352
426
  """
353
- attempts = 1 if spec.port is not None else 2
427
+ attempts = (
428
+ 1 if spec.port is not None else 1 + self._config.port_retry_attempts
429
+ )
354
430
  last_exc: Exception | None = None
355
431
  for _attempt in range(attempts):
356
432
  port = spec.port if spec.port is not None else _pick_ephemeral_port()
@@ -368,6 +444,10 @@ class SwapManager:
368
444
  )
369
445
  with contextlib.suppress(Exception):
370
446
  await stop_process(self._app, proc.id)
447
+ # [Unreleased] registry-litter fix: without this, every failed
448
+ # readiness attempt leaves one "stopped" row in the registry
449
+ # forever (1 + port_retry_attempts rows per failed load).
450
+ self._remove_from_registry(proc.id)
371
451
  return None, last_exc
372
452
 
373
453
  async def _spawn(self, spec: SwapModelSpec, port: int) -> ManagedProcess:
@@ -394,6 +474,10 @@ class SwapManager:
394
474
  # provider). H-2: exempt from launcher auto-restart (crash
395
475
  # recovery = next-request re-spawn under the per-model lock).
396
476
  swap_managed=True,
477
+ # [Unreleased]: catalog model name, surfaced by
478
+ # GET /api/launcher/processes as "swap_model" so the /launcher
479
+ # UI can show which swap catalog entry a process backs.
480
+ swap_model=spec.name,
397
481
  )
398
482
 
399
483
  async def _await_ready(self, proc: ManagedProcess) -> bool:
@@ -457,6 +541,10 @@ class SwapManager:
457
541
  with contextlib.suppress(Exception):
458
542
  await stop_process(self._app, proc_id)
459
543
  await self._deregister(spec)
544
+ # [Unreleased] registry-litter fix: a TTL-unloaded swap process
545
+ # is not "history" the way a manually stopped one is — leaving
546
+ # it would grow the registry by one row per load/unload cycle.
547
+ self._remove_from_registry(proc_id)
460
548
  logger.info(
461
549
  "swap-unload",
462
550
  extra={"model": spec.name, "reason": reason, "proc_id": proc_id},
@@ -276,14 +276,16 @@ For a more realistic example that mixes swap with an always-on backend like Olla
276
276
  | Field | Type | Default | Description |
277
277
  |---|---|---|---|
278
278
  | `enabled` | bool | `false` | Enable on-demand swap. `false` preserves manual-only startup exactly as before (no impact on existing deployments) |
279
- | `ttl_seconds` | float \| null | `1800.0` | Seconds of no in-flight requests after which the process is auto-stopped. `null` = disabled (runs until explicitly stopped), `0` = unload as soon as the last request completes. Phase 1 has a single global value onlyno per-model override |
279
+ | `ttl_seconds` | float \| null | `1800.0` | Seconds of no in-flight requests after which the process is auto-stopped. `null` = disabled (runs until explicitly stopped), `0` = unload as soon as the last request completes. This is the global default shared by all models `models[].ttl_seconds` overrides it per model ([Unreleased]) |
280
280
  | `readiness_timeout_s` | float | `120.0` (1–1800) | Upper bound (seconds) a **request** will wait for an on-demand spawn to become ready before the dispatch hook raises a retryable `AdapterError`. **Distinct from `launcher.readiness_timeout_s` below (default 300s)** — this one bounds "how long one request waits," that one bounds "the general readiness-monitoring window for a process" |
281
281
  | `sweep_interval_s` | float | `15.0` (1–600) | How often the TTL sweeper background task scans for idle processes |
282
+ | `port_retry_attempts` | int | `2` (0–5) | [Unreleased] Number of additional retries (each on a freshly picked ephemeral port) after a failed startup, for catalog entries that leave `models[].port` unset. `0` = no retry. Irrelevant for a fixed `port` (always exactly one attempt). Does not close the TOCTOU window itself — see `models[].port` below |
282
283
  | `inject_auto_router_rules` | bool | `true` | Whether to auto-generate an `auto_router` rule per catalog model (`id: swap:<name>`, `model_pattern` is an exact match on the name). Set `false` to wire routing yourself (e.g. via the `X-CodeRouter-Profile` header) |
283
284
  | `models[].name` | str (required) | — | Logical name matched against the request's `model` field. The provider name and dedicated profile name are automatically `launcher-swap-<name>` |
284
285
  | `models[].backend` | `"llama.cpp" \| "vllm" \| "mlx"` (required) | — | Same backend set as the manual Launcher UI |
285
286
  | `models[].model_path` | str (required) | — | Absolute or `~`-relative model file path. Must be **under `launcher.model_dirs`** — re-validated both at config load and at spawn time (defense against path traversal) |
286
- | `models[].port` | int \| null | `null` | Fixed port recommended. When unset, an OS-assigned ephemeral port is used, with one retry on a different port if startup fails (best-effort — no strong TOCTOU guarantee) |
287
+ | `models[].port` | int \| null | `null` | Fixed port recommended. When unset, an OS-assigned ephemeral port is used, retrying on a fresh port up to `launcher.swap.port_retry_attempts` times (default 2) if startup fails. Best-effort — the gap between picking the port and the child actually binding it (TOCTOU) is never fully closed. Use a fixed port if you need a strong guarantee |
288
+ | `models[].ttl_seconds` | float \| null | `null` | [Unreleased] Per-model TTL override. `null` (default) = follow the global `launcher.swap.ttl_seconds`. `0` means the same thing as the global field's `0` (unload as soon as the last lease releases), scoped to just this model |
287
289
  | `models[].option_profile` | str \| null | `null` | Name of an existing preset in `launcher.option_profiles[backend]`. A name that doesn't exist is a config-load error |
288
290
  | `models[].num_ctx` | int | `8192` (≥256) | Baseline used for KV estimation / launch parameters |
289
291
  | `models[].extra_args` | str | `""` | One-off extra CLI flags (a single string, parsed with `shlex`; re-specifying the model/draft model is rejected) |
@@ -276,14 +276,16 @@ Ollama など常用の backend と併用する、より実運用寄りの設定
276
276
  | フィールド | 型 | 既定値 | 説明 |
277
277
  |---|---|---|---|
278
278
  | `enabled` | bool | `false` | オンデマンドスワップを有効化。`false` ならこれまで通り手動起動のみ(既存デプロイに影響なし) |
279
- | `ttl_seconds` | float \| null | `1800.0` | 進行中リクエストが無い状態がこの秒数続くとプロセスを自動停止。`null` = 無効(明示停止まで起動し続ける)、`0` = 最後のリクエスト完了で即アンロード。Phase 1 はグローバル1値のみ(モデル毎の上書きは未対応) |
279
+ | `ttl_seconds` | float \| null | `1800.0` | 進行中リクエストが無い状態がこの秒数続くとプロセスを自動停止。`null` = 無効(明示停止まで起動し続ける)、`0` = 最後のリクエスト完了で即アンロード。全モデル共通のグローバル既定値 `models[].ttl_seconds` で個別上書き可能([Unreleased]) |
280
280
  | `readiness_timeout_s` | float | `120.0`(1〜1800) | オンデマンド起動したモデルの準備完了を**リクエスト側が待つ**上限秒数。超えると dispatch フックが retryable な `AdapterError` を送出する。**下記の `launcher.readiness_timeout_s`(既定 300s)とは別物** — こちらは「1リクエストが待つ上限」、あちらは「プロセスの readiness 監視全般の上限」 |
281
281
  | `sweep_interval_s` | float | `15.0`(1〜600) | TTL 監視(sweeper)がアイドルプロセスを走査する間隔 |
282
+ | `port_retry_attempts` | int | `2`(0〜5) | [Unreleased] `models[].port` が未指定のカタログエントリに対する、起動失敗時の追加リトライ回数(毎回新しいエフェメラルポートを取り直す)。`0` ならリトライなし。固定ポート指定時は無関係(常に1回のみ)。TOCTOU窓自体は解消しない — 詳細は `models[].port` の説明を参照 |
282
283
  | `inject_auto_router_rules` | bool | `true` | カタログの各モデル名ごとに `auto_router` ルール(`id: swap:<name>`、`model_pattern` は名前の完全一致)を自動生成するか。`false` にした場合は `X-CodeRouter-Profile` ヘッダ等で自分でルーティングを配線する |
283
284
  | `models[].name` | str(必須) | — | リクエストの `model` フィールドと照合される論理名。provider 名・専用プロファイル名は自動的に `launcher-swap-<name>` になる |
284
285
  | `models[].backend` | `"llama.cpp" \| "vllm" \| "mlx"`(必須) | — | 手動 Launcher UI と同じバックエンド種別 |
285
286
  | `models[].model_path` | str(必須) | — | モデルファイルの絶対または `~` 相対パス。**必ず `launcher.model_dirs` の配下**である必要があり、設定ロード時と起動時の2回検証される(パストラバーサル対策) |
286
- | `models[].port` | int \| null | `null` | 固定ポート推奨。省略時は OS 割当のエフェメラルポートを使い、起動失敗時に別ポートで1回だけリトライする(best-effort、強い TOCTOU 保証はない) |
287
+ | `models[].port` | int \| null | `null` | 固定ポート推奨。省略時は OS 割当のエフェメラルポートを使い、起動失敗時に `launcher.swap.port_retry_attempts` 回まで(既定2回)別ポートでリトライする。best-effort — pick してから子プロセスが実際に bind するまでの間隙(TOCTOU)は解消されない。強い保証が要る場合は固定ポートを使うこと |
288
+ | `models[].ttl_seconds` | float \| null | `null` | [Unreleased] このモデルだけの TTL 上書き。`null`(既定) = グローバルの `launcher.swap.ttl_seconds` に従う。`0` を指定するとグローバルの `0` と同じ意味(最後のリース解放で即アンロード)がこのモデルにだけ適用される |
287
289
  | `models[].option_profile` | str \| null | `null` | `launcher.option_profiles[backend]` の既存プリセット名。存在しない名前を指定すると設定ロード時にエラーになる |
288
290
  | `models[].num_ctx` | int | `8192`(≥256) | KV 見積り・起動パラメータの基準値 |
289
291
  | `models[].extra_args` | str | `""` | 追加 CLI フラグ(1本の文字列。`shlex` でパース。モデル/draft モデルの再指定は不可) |
@@ -1,7 +1,8 @@
1
1
  # オーケストレーション・コンパニオン設計書(Ecosystem 層、構想)
2
2
 
3
+ > 編集注記(2026-07-12): リンク整理。`future.md` は現リポジトリに非収録のため、リンクは注記化または現存ドキュメントへの付け替え済み(記述内容自体は変更していない)。
3
4
  > ステータス: **構想(Concept)**。実装前。Core/Plugin の変更は伴わない。
4
- > 正典: [`docs/inside/future.md`](../inside/future.md) §5(2026-06-27 追記)・§1.2(三層モデル)・§2.5(2026-07-11 追記)
5
+ > 正典: `future.md`(当時の内部設計メモ。現リポジトリには未収録)§5(2026-06-27 追記)・§1.2(三層モデル)・§2.5(2026-07-11 追記)。§1.2 の判定基準に相当する記述は [`agent-cli-plugin-extraction.md`](./agent-cli-plugin-extraction.md) §1.3、§2.5 のopusplan構成例・既知の制限(G1/G6)に相当する記述は [`subagent-routing.md`](../guides/subagent-routing.md) §5(a)・§7 に移設されている。
5
6
  > 前提: agent_cli Phase 1 完結(v2.7.10、[`external-agents-adapter.md`](./external-agents-adapter.md))
6
7
  > 作成: 2026-07-11 / 出典: 作者方向性指示 `_article/direction-brief-2026-07-11.md`・解析書 `_article/analysis-direction-2026-07.md` D-補足
7
8
 
@@ -60,7 +61,7 @@ Voice Bridge と同型で、CodeRouter Plugin SDK は使わない。future.md §
60
61
  [推論バックエンド] — agent_cli(claude opus 等) / ローカル / クラウド混在
61
62
  ```
62
63
 
63
- `providers.yaml` 側の構成例は future.md §2.5「opusplan 型構成例」を参照(本書では重複させない)。
64
+ `providers.yaml` 側の構成例は [`subagent-routing.md`](../guides/subagent-routing.md) §5(a)「opusplan 型」を参照(本書では重複させない。旧 future.md §2.5「opusplan 型構成例」に相当する記述。future.md は当時の内部設計メモで、現リポジトリには未収録)。
64
65
 
65
66
  ---
66
67
 
@@ -71,10 +72,10 @@ Voice Bridge と同型で、CodeRouter Plugin SDK は使わない。future.md §
71
72
  - 役割別 profile 解決、agent_cli backend、fallback、L1〜L6 guards、plugin-memory/compress の透過適用は**すべて既存資産で足りる**。追加実装は不要(future.md §2.5 (1) 確信度 HIGH)。
72
73
  - `X-CodeRouter-Profile` ヘッダの precedence 機構は v1.6 以降既存。
73
74
 
74
- **G1/G3future.md §2.5 (4) のギャップ表)が欲しくなる条件**:
75
+ **G1/G3(当時の内部設計メモ future.md §2.5 (4) のギャップ表より。現リポジトリには未収録。G1 は現存記述として [`subagent-routing.md`](../guides/subagent-routing.md) §7 に対応項目あり)が欲しくなる条件**:
75
76
 
76
- - G1(複合条件マッチャ): orchestrator が profile ヘッダを明示付与している限り不要。**auto_router の保険ルールを複雑化させたい場合のみ**(例: 「コード密度が高く *かつ* ツール宣言あり」を 1 ルールで表現したい)に欲しくなる。orchestrator 側が常にヘッダを付ける設計なら発生しない。
77
- - G3(ヘッダ/ツール名ベースのルール条件): 同上。orchestrator が特定 MCP ツール名に応じて動的に profile を切り替えたい場合に初めて必要になる。
77
+ - G1(複合条件マッチャ): orchestrator が profile ヘッダを明示付与している限り不要。**auto_router の保険ルールを複雑化させたい場合のみ**(例: 「コード密度が高く *かつ* ツール宣言あり」を 1 ルールで表現したい)に欲しくなる。orchestrator 側が常にヘッダを付ける設計なら発生しない(cf. [`subagent-routing.md`](../guides/subagent-routing.md) §7 の G1 記述)。
78
+ - G3(ヘッダ/ツール名ベースのルール条件): 同上。orchestrator が特定 MCP ツール名に応じて動的に profile を切り替えたい場合に初めて必要になる(future.md 由来。現リポジトリには対応する記述なし)。
78
79
 
79
80
  **明確に CodeRouter 側に要求しないもの**: 段階判定(今が Plan か実行かの意味的分類、G2)・深度連動 routing(G4)・並列 fan-out(G5)はすべて orchestrator 側の責務であり、wire 層に持ち込まない。
80
81
 
@@ -104,6 +105,6 @@ Voice Bridge と同型で、CodeRouter Plugin SDK は使わない。future.md §
104
105
 
105
106
  ## 関連ドキュメント
106
107
 
107
- - [`docs/inside/future.md`](../inside/future.md) §5・§1.2・§2.5 正典
108
+ - `future.md` §5・§1.2・§2.5 — 正典(当時の内部設計メモ future.md より。現リポジトリには未収録)。§1.2 相当は [`docs/designs/agent-cli-plugin-extraction.md`](./agent-cli-plugin-extraction.md) §1.3、§2.5 の一部(opusplan構成例・G1/G6)相当は [`docs/guides/subagent-routing.md`](../guides/subagent-routing.md) §5(a)・§7 を参照
108
109
  - [`docs/designs/external-agents-adapter.md`](./external-agents-adapter.md) — agent_cli backend(本書が利用する CodeRouter 側の対応機能)
109
- - `_OUTPUTS/04-計画-方向性/multiagent/` — 実機プロトタイプ一式(orchestrator/safe-edit/ベンチ)
110
+ - `_OUTPUTS/04-計画-方向性/multiagent/` — 実機プロトタイプ一式(orchestrator/safe-edit/ベンチ。当時の作業ディレクトリ参照。現リポジトリには未収録)
@@ -11,7 +11,7 @@
11
11
  # in plan.md §11.B; once granted, this name will become an alias and
12
12
  # `coderouter` will become the canonical distribution name.
13
13
  name = "coderouter-cli"
14
- version = "2.9.2"
14
+ version = "2.9.3"
15
15
  description = "Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible."
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.12"