You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 effect — ChannelBridge 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:
Channel override — src/channels/model-overrides.ts::resolveChannelModelOverride reads cfg.channels.modelByChannel, matches by parent-group-id + topic, returns { provider, model }.
Heartbeat override — src/infra/heartbeat-runner.ts:775-786 reads heartbeat?.model?.trim() and populates replyOpts.heartbeatModelOverride.
Reply pipeline — src/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.
FollowupRun — the overridden provider/model flow into FollowupRun.run.provider/.model.
But then — agent-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):
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.
Middleware Boundary Principle: see product/strategy.md in the HQ repo; the rule is "agents bring their own model selection; RemoteClaw provides channel routing and session management"
Audit source: targeted provider/model obsolescence audit, fork HEAD 44b4f92b12, 2026-04-13
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 effect —
ChannelBridgedoesn't honor the overridden values, andChannelMessage(the bridge's input type) has nomodelfield 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 throughprovider/modelfields 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:
src/channels/model-overrides.ts::resolveChannelModelOverridereadscfg.channels.modelByChannel, matches by parent-group-id + topic, returns{ provider, model }.src/infra/heartbeat-runner.ts:775-786readsheartbeat?.model?.trim()and populatesreplyOpts.heartbeatModelOverride.src/auto-reply/reply/get-reply.ts:94-104readsopts.heartbeatModelOverrideand assigns to localprovider/model. Lines 195-223 consumeresolveChannelModelOverrideand overwrite the same locals.provider/modelflow intoFollowupRun.run.provider/.model.src/auto-reply/reply/agent-runner-execution.ts:220-221references them.But then —
agent-runner-execution.ts:347-348constructsChannelBridgewith:— the actual CLI runtime is selected from agent config, NOT from the override-derived
provider. AndChannelMessage(the bridge's input payload) has nomodelfield at all.Remaining side effects (the override values are computed for these, but none changes agent execution):
cliSessionIds[provider]lookup keying (potential bug when overrideprovidermismatches agent runtime)onModelSelectedUX notificationfallbackTransitionnotice stateDecision Needed
Are per-message / per-channel model overrides in scope for v0.1.0?
Option A — Keep and repair (rejected per Middleware Boundary Principle):
Option B — Gut (recommended):
Files to Delete / Modify if Option B
Delete
src/channels/model-overrides.tssrc/channels/model-overrides.test.tssrc/infra/heartbeat-runner.model-override.test.tsModify
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 forresolveChannelModelOverridesrc/auto-reply/types.ts:45— removeheartbeatModelOverridefieldsrc/infra/heartbeat-runner.ts:775-786— removeheartbeat?.model?.trim()read +replyOpts.heartbeatModelOverrideassignmentsrc/config/zod-schema.channels-core.ts(or wherevermodelByChannelis defined) — removemodelByChannelfrom the channel config schemamodelfield from heartbeat configCleanup
onModelSelected/fallbackTransitionUX 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
pnpm checkpassespnpm testpassesgit grep -rn "modelByChannel\|heartbeatModelOverride\|resolveChannelModelOverride" src/returns zero hitsOut 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 bygateway/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.model-catalog.ts— different Pi-era concern, tracked separately.References
product/strategy.mdin the HQ repo; the rule is "agents bring their own model selection; RemoteClaw provides channel routing and session management"44b4f92b12, 2026-04-13