Skip to content

gut(agents): remove model-selection.ts — CLI runtimes own model selection per MBP (#2455)#2456

Merged
alexey-pelykh merged 1 commit intomainfrom
gut/2455-agents-model-selection
Apr 21, 2026
Merged

gut(agents): remove model-selection.ts — CLI runtimes own model selection per MBP (#2455)#2456
alexey-pelykh merged 1 commit intomainfrom
gut/2455-agents-model-selection

Conversation

@alexey-pelykh
Copy link
Copy Markdown

Closes #2455.

Summary

Delete src/agents/model-selection.ts (170 LOC) per Middleware Boundary Principle. Model selection is owned by CLI runtimes (Claude, Gemini, Codex, OpenCode); middleware still reads the configured default from cfg.agents.defaults.*, which is legitimate config-reading, not selection.

Sibling precedents: #2414 (Discord model-picker gut), #2449/#2451 (Mattermost model-picker gut). Surfaced during #2454 H3 restoration; closes the pending-gutting debt at #2355.

Strategy

Live config-reading utilities relocated to src/agents/provider-utils.ts (their natural home alongside existing provider normalization + model ref parsing helpers):

  • buildModelAliasIndex
  • resolveConfiguredModelRef
  • resolveDefaultModelForAgent
  • ModelAliasIndex type

Dead stubs eliminated (zero production callers — verified via grep):

  • resolveModelRefFromString (only the test mock consumed it)
  • resolveThinkingDefault (always returned undefined; caller block in chat.ts was unreachable — block removed)
  • getModelRefStatus (always returned undefined)
  • inferUniqueProviderFromConfiguredModels (always returned undefined; caller block in session-utils.ts was unreachable — block removed)
  • splitTrailingAuthProfile (internal helper; only served resolveModelRefFromString)

Callers migrated (14 total)

The issue listed 13 callers; investigation found 14: issue's path-qualified grep missed same-directory relative-path imports in src/agents/{provider-capabilities,transcript-policy}.ts and missed the vi.mock string arg in get-reply.test-mocks.ts (not tracked by the zombie-gate since it is not an import statement). Issue's src/plugin-sdk/mattermost.ts entry was phantom — no import existed.

Category Files
Re-export redirects agent-runner, directive-handling.model-picker, session-store, defaults, gateway-cli-backend.live.test, provider-capabilities, transcript-policy
Type relocation directive-handling
Live-function callers status.summary, sticker-cache, session-utils (multi-import + dead inferUniqueProviderFromConfiguredModels block), chat (imports dropped + dead thinkingLevel fallback removed)
Test mocks agent.test (w/ modelSelectionModuleproviderUtilsModule rename), run.test-harness (retarget + stale mock helpers removed), get-reply.test-mocks (dead vi.mock block removed)

Zombie-gate updates

  • scripts/check-no-zombie-imports.mjs: removed "agents/model-selection" from fallbackDeadModulePatterns + its explanatory block
  • scripts/data/zombie-import-allowlist.json: -12 entries (issue said 13, actual allowlist had 12 — plugin-sdk/mattermost was phantom)
  • .zombie-import-allowlist-baseline: 23 → 11

Baselines self-healed

  • .fork-boundary-mock-baseline: 133 → 132 (direct: removed vi.mock in get-reply.test-mocks.ts)
  • .obsolescence-baseline: 134 → 49 (this PR: -1; pre-existing unbaselined improvements: -84 — verified via stash-based pre-change gate run)

Pre-existing issue surfaced (not caused by this PR, not blocking)

src/gateway/session-utils.test.ts has 20 failing tests in resolveSessionModelIdentityRef/listSessionsFromStore blocks asserting provider inference via inferUniqueProviderFromConfiguredModels. Those tests fail on main too — inferUniqueProviderFromConfiguredModels was already a stub returning undefined there. The gateway suite is opt-in via REMOTECLAW_TEST_INCLUDE_GATEWAY=1 (see scripts/test-parallel.mjs:115); default pnpm test in CI does not run it. This PR removed the already-non-functional caller block; behavior is identical. Follow-up: delete the obsolete tests or restore a config-reading implementation.

Test plan

  • pnpm tsgo — exit 0
  • pnpm lint — 0 warnings, 0 errors
  • pnpm format:check — clean (5115 files)
  • pnpm test — 7298 passed, 3 skipped, 0 failed (288 unit + 7010 full suite)
  • node scripts/check-no-zombie-imports.mjs — passed (11 == baseline 11)
  • node scripts/check-stub-debt.mjs@ts-expect-error: 12 == baseline 12; fork-boundary-mock: 132 == baseline 132
  • node scripts/check-throwing-stub-callers.mjs — 0 violations
  • node scripts/check-attestations.mjs — 92 modules, 315 attestations
  • node scripts/check-obsolescence-audit.mjs — 49 == baseline 49
  • bash scripts/ci/check-rebrand-leakage.sh — no files to check
  • grep for model-selection refs in src/extensions/apps/packages/ui/test/scripts — zero import hits (only provenance comment in provider-utils.ts)

🤖 Generated with Claude Code

…tion per MBP (#2455)

Delete src/agents/model-selection.ts (170 LOC) per Middleware Boundary
Principle. Model selection is owned by CLI runtimes (Claude, Gemini, Codex,
OpenCode); middleware only reads the configured default from cfg.agents.defaults.

Live config-reading utilities relocated to src/agents/provider-utils.ts
(their natural home alongside existing provider normalization + model ref
parsing helpers):
- buildModelAliasIndex
- resolveConfiguredModelRef
- resolveDefaultModelForAgent
- ModelAliasIndex type

Dead stubs eliminated (zero production callers):
- resolveModelRefFromString (only the test-mock consumed it)
- resolveThinkingDefault (always returned undefined; caller block in chat.ts
  was unreachable — removed)
- getModelRefStatus (always returned undefined)
- inferUniqueProviderFromConfiguredModels (always returned undefined; caller
  block in session-utils.ts was unreachable — removed)
- splitTrailingAuthProfile (internal helper; only served resolveModelRefFromString)

14 callers migrated (issue listed 13 + 1 phantom; this PR additionally covered
provider-capabilities.ts, transcript-policy.ts, and get-reply.test-mocks.ts
which were missed by the issue's path-qualified grep):
- Category A (re-export redirects): agent-runner, directive-handling.model-picker,
  session-store, defaults, gateway-cli-backend.live.test, provider-capabilities,
  transcript-policy
- Category B (type relocation): directive-handling
- Category C (live-function callers): status.summary, sticker-cache, session-utils,
  chat (imports dropped + dead thinkingLevel fallback removed)
- Category D (test mocks): agent.test (with modelSelectionModule →
  providerUtilsModule rename), run.test-harness (mock retarget + stale mock
  helpers removed), get-reply.test-mocks (dead vi.mock block removed)

Zombie-import gate updated:
- scripts/check-no-zombie-imports.mjs: removed "agents/model-selection" from
  fallbackDeadModulePatterns + explanatory block
- scripts/data/zombie-import-allowlist.json: -12 entries
- .zombie-import-allowlist-baseline: 23 → 11

Baselines self-healed:
- .fork-boundary-mock-baseline: 133 → 132 (direct: removed vi.mock in
  get-reply.test-mocks.ts)
- .obsolescence-baseline: 134 → 49 (this PR: -1; pre-existing unbaselined
  improvements: -84 — verified via stash-based pre-change gate run)

Sibling precedents: #2414 (Discord model-picker gut), #2449/#2451 (Mattermost
model-picker gut). Surfaced during #2454 H3 restoration; closes #2355 debt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexey-pelykh alexey-pelykh requested a review from Copilot April 21, 2026 15:43
@alexey-pelykh alexey-pelykh enabled auto-merge (squash) April 21, 2026 15:43
@alexey-pelykh alexey-pelykh merged commit a7bcfd0 into main Apr 21, 2026
19 checks passed
@alexey-pelykh alexey-pelykh deleted the gut/2455-agents-model-selection branch April 21, 2026 15:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes the legacy src/agents/model-selection.ts module per the Middleware Boundary Principle by relocating the remaining config-reading/model-ref utilities into src/agents/provider-utils.ts, and updates all in-repo callsites and gate baselines accordingly.

Changes:

  • Delete src/agents/model-selection.ts and migrate imports to src/agents/provider-utils.ts.
  • Remove unreachable/dead caller blocks that depended on stubbed model-selection exports.
  • Update zombie-import gate allowlist/baselines and related scripts to reflect the removal.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/telegram/sticker-cache.ts Switch default-model resolution import to provider-utils.
src/gateway/session-utils.ts Migrate imports; remove unreachable inferred-provider fallback block.
src/gateway/server-methods/chat.ts Drop unused model-selection import and unreachable thinking-default fallback.
src/gateway/gateway-cli-backend.live.test.ts Update parseModelRef import to provider-utils.
src/cron/isolated-agent/run.test-harness.ts Retarget vitest mock from model-selection to provider-utils; remove stale mock helpers.
src/config/defaults.ts Update provider/model parsing imports to provider-utils.
src/commands/status.summary.ts Update configured-model resolution import to provider-utils.
src/commands/agent/session-store.ts Update isCliProvider import to provider-utils.
src/commands/agent.test.ts Rename module import to providerUtilsModule and update mocks accordingly.
src/auto-reply/reply/get-reply.test-mocks.ts Remove dead vi.mock for deleted model-selection helper.
src/auto-reply/reply/directive-handling.ts Relocate ModelAliasIndex type import to provider-utils.
src/auto-reply/reply/directive-handling.model-picker.ts Update ModelRef/normalizeProviderId import source.
src/auto-reply/reply/agent-runner.ts Update isCliProvider import source.
src/agents/transcript-policy.ts Update normalizeProviderId import source.
src/agents/provider-utils.ts Add config-reading model resolution utilities formerly in model-selection.ts.
src/agents/provider-capabilities.ts Update normalizeProviderId import source.
src/agents/model-selection.ts Delete the module entirely.
scripts/data/zombie-import-allowlist.json Remove allowlisted agents/model-selection callsites.
scripts/check-no-zombie-imports.mjs Remove agents/model-selection from fallback dead-module patterns.
.zombie-import-allowlist-baseline Rebaseline count to match updated allowlist.
.obsolescence-baseline Update baseline after audit changes.
.fork-boundary-mock-baseline Update baseline after removing one mock entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +227 to +294
export function buildModelAliasIndex(params: {
cfg: RemoteClawConfig;
defaultProvider: string;
allowPluginNormalization?: boolean;
}): ModelAliasIndex {
const byAlias = new Map<string, { alias: string; ref: ModelRef }>();
const byKey = new Map<string, string[]>();

const rawModels = (params.cfg as any).agents?.defaults?.models ?? {};
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
if (!parsed) {
continue;
}
const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim();
if (!alias) {
continue;
}
const aliasKey = normalizeAliasKey(alias);
byAlias.set(aliasKey, { alias, ref: parsed });
const key = modelKey(parsed.provider, parsed.model);
const existing = byKey.get(key) ?? [];
existing.push(alias);
byKey.set(key, existing);
}

return { byAlias, byKey };
}

export function resolveConfiguredModelRef(params: {
cfg: RemoteClawConfig;
defaultProvider: string;
defaultModel: string;
allowPluginNormalization?: boolean;
}): ModelRef {
const rawModel = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model) ?? "";
if (rawModel) {
const trimmed = rawModel.trim();
const aliasIndex = buildModelAliasIndex({
cfg: params.cfg,
defaultProvider: params.defaultProvider,
allowPluginNormalization: params.allowPluginNormalization,
});
if (!trimmed.includes("/")) {
const aliasKey = normalizeAliasKey(trimmed);
const aliasMatch = aliasIndex.byAlias.get(aliasKey);
if (aliasMatch) {
return aliasMatch.ref;
}
}
const parsed = parseModelRef(trimmed, params.defaultProvider);
if (parsed) {
return parsed;
}
}
return { provider: params.defaultProvider, model: params.defaultModel };
}

export function resolveDefaultModelForAgent(params: {
cfg: RemoteClawConfig;
agentId?: string;
}): ModelRef {
return resolveConfiguredModelRef({
cfg: params.cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newly added config-reading resolution helpers (buildModelAliasIndex, resolveConfiguredModelRef, resolveDefaultModelForAgent) are non-trivial (alias handling + fallback behavior) but src/agents/provider-utils.test.ts currently only covers provider normalization/CLI detection. Add unit tests for alias resolution, provider/model parsing, and default fallback to prevent regressions as config shape evolves.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to 13
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { RemoteClawConfig } from "../config/config.js";
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@typescript-eslint/no-explicit-any is disabled for the whole module, but RemoteClawConfig already types cfg.agents.defaults.models (see src/config/types.agent-defaults.ts). Instead of (params.cfg as any)..., access params.cfg.agents?.defaults?.models directly (and type entryRaw as AgentModelEntryConfig if needed) so the global eslint disable can be removed or scoped to a single line.

Suggested change
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { RemoteClawConfig } from "../config/config.js";
import type { RemoteClawConfig } from "../config/config.js";
import type { AgentModelEntryConfig } from "../config/types.agent-defaults.js";

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +270
const aliasIndex = buildModelAliasIndex({
cfg: params.cfg,
defaultProvider: params.defaultProvider,
allowPluginNormalization: params.allowPluginNormalization,
});
if (!trimmed.includes("/")) {
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveConfiguredModelRef builds the full alias index unconditionally, even when trimmed already contains / (so alias lookup is skipped). Consider only calling buildModelAliasIndex inside the !trimmed.includes("/") branch to avoid unnecessary work on hot paths like session listing/status.

Suggested change
const aliasIndex = buildModelAliasIndex({
cfg: params.cfg,
defaultProvider: params.defaultProvider,
allowPluginNormalization: params.allowPluginNormalization,
});
if (!trimmed.includes("/")) {
if (!trimmed.includes("/")) {
const aliasIndex = buildModelAliasIndex({
cfg: params.cfg,
defaultProvider: params.defaultProvider,
allowPluginNormalization: params.allowPluginNormalization,
});

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gut(agents): remove src/agents/model-selection.ts — CLI runtimes own model selection per MBP

2 participants