Skip to content

gut: remove decoratively-wired Pi-era per-message model override flow #2339

@alexey-pelykh

Description

@alexey-pelykh

Summary

The fork retains a Pi-era per-message model override pathway (channel overrides + heartbeat overrides) that is wired into the reply pipeline but has no observable effectChannelBridge doesn't honor the overridden values, and ChannelMessage (the bridge's input type) has no model field at all. This is decoratively alive code that should be gutted.

Background

In the OpenClaw/Pi-era architecture, an agent could have its model overridden per-message — e.g., per-channel (cfg.channels.modelByChannel) or per-heartbeat run (cfg.heartbeat.model). The reply pipeline threaded the override through provider/model fields down to the in-process Pi runner.

In the RemoteClaw architecture, the agent-runtime binding is fixed at agent definition time. The CLI runtime (Claude/Gemini/Codex/OpenCode) is selected via resolveAgentRuntimeOrThrow(cfg, agentId), not per-message. Model selection is internal to the CLI runtime subprocess.

Evidence of Decorative Wiring

The override still flows through the reply pipeline:

  1. Channel overridesrc/channels/model-overrides.ts::resolveChannelModelOverride reads cfg.channels.modelByChannel, matches by parent-group-id + topic, returns { provider, model }.
  2. Heartbeat overridesrc/infra/heartbeat-runner.ts:775-786 reads heartbeat?.model?.trim() and populates replyOpts.heartbeatModelOverride.
  3. Reply pipelinesrc/auto-reply/reply/get-reply.ts:94-104 reads opts.heartbeatModelOverride and assigns to local provider/model. Lines 195-223 consume resolveChannelModelOverride and overwrite the same locals.
  4. FollowupRun — the overridden provider/model flow into FollowupRun.run.provider/.model.
  5. Agent runnersrc/auto-reply/reply/agent-runner-execution.ts:220-221 references them.

But thenagent-runner-execution.ts:347-348 constructs ChannelBridge with:

provider: resolveAgentRuntimeOrThrow(cfg, agentId)

— the actual CLI runtime is selected from agent config, NOT from the override-derived provider. And ChannelMessage (the bridge's input payload) has no model field at all.

Remaining side effects (the override values are computed for these, but none changes agent execution):

  • cliSessionIds[provider] lookup keying (potential bug when override provider mismatches agent runtime)
  • onModelSelected UX notification
  • fallbackTransition notice state

Decision Needed

Are per-message / per-channel model overrides in scope for v0.1.0?

Option A — Keep and repair (rejected per Middleware Boundary Principle):

  • Redefine semantics as per-channel runtime selection (e.g., "use Claude in Slack, Gemini in Discord")
  • ChannelBridge needs to honor the override
  • Substantial work; reintroduces complexity that was deliberately removed

Option B — Gut (recommended):

  • The CLI runtime binding is an agent-level config decision, not a channel or message decision
  • Pi-era cruft that leaked through the gutting waves

Files to Delete / Modify if Option B

Delete

  • src/channels/model-overrides.ts
  • src/channels/model-overrides.test.ts
  • src/infra/heartbeat-runner.model-override.test.ts

Modify

  • src/auto-reply/reply/get-reply.ts — remove the override consumption block at lines 94-104 (heartbeat) and 195-223 (channel)
  • src/auto-reply/reply/get-reply.test-mocks.ts — remove mock for resolveChannelModelOverride
  • src/auto-reply/types.ts:45 — remove heartbeatModelOverride field
  • src/infra/heartbeat-runner.ts:775-786 — remove heartbeat?.model?.trim() read + replyOpts.heartbeatModelOverride assignment
  • src/config/zod-schema.channels-core.ts (or wherever modelByChannel is defined) — remove modelByChannel from the channel config schema
  • Heartbeat config schema — remove model field from heartbeat config

Cleanup

  • Any remaining consumers of onModelSelected / fallbackTransition UX state — verify whether they're tied to this flow or to a separate path. If tied, clean up together; if separate, leave alone.

Acceptance Criteria

  • Decision documented in PR body (Option A or B, with rationale)
  • If Option B: all files/sections above deleted or modified
  • pnpm check passes
  • pnpm test passes
  • Verification: git grep -rn "modelByChannel\|heartbeatModelOverride\|resolveChannelModelOverride" src/ returns zero hits
  • Manual smoke test: heartbeat runs still work (without model override)
  • Manual smoke test: per-channel reply still works (without override)
  • CI zombie-import gate passes

Out of Scope

  • src/agents/model-selection.ts — this file is KEEP (17+ live consumers for model-ref parsing, alias resolution). It has a 3-function stub subset (resolveThinkingDefault, getModelRefStatus, inferUniqueProviderFromConfiguredModels) where one is called by gateway/server-methods/chat.ts — that belongs to gut(auto-reply): finish thinking-level and model-selection sweep — follow-up to #2335 #2336 (thinking-level sweep), not this issue.
  • Telegram vision capability check via model-catalog.ts — different Pi-era concern, tracked separately.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    gutRemoving dead upstream subsystemsneeds-discussionApproach is opinionated or uncertain — needs team alignment before implementation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions