Summary
src/agents/agent-scope.ts:491-493 is an unconditional-throw stub:
export function resolveAgentRuntimeOrThrow(..._args: unknown[]): never {
throw new Error("resolveAgentRuntimeOrThrow is not available in RemoteClaw fork");
}
Three production callers invoke it during every agent dispatch. Every Slack auto-reply, every /agent CLI invocation, and every cron isolated-agent run crashes with the user-visible:
⚠️ Agent failed before reply: resolveAgentRuntimeOrThrow is not available in RemoteClaw fork.
Logs: remoteclaw logs --follow
Live callers (unmigrated)
| File |
Line |
Context |
src/auto-reply/reply/agent-runner-execution.ts |
350 |
ChannelBridge.provider in auto-reply / Slack path |
src/commands/agent.ts |
189 |
runAgentAttempt for /agent CLI |
src/cron/isolated-agent/run.ts |
342 |
resolvedRuntime in cron isolated agent |
src/cron/isolated-agent/run.ts |
389 |
ChannelBridge.provider in cron retry loop |
The sibling resolveAgentRuntime (non-throwing, returns string | undefined) exists at src/agents/agent-scope.ts:440 and works correctly; it's used at src/cron/isolated-agent/run.ts:202 for telemetry.
Error propagation path
- Caller invokes
resolveAgentRuntimeOrThrow(cfg, agentId) → throws.
- Error propagates through
withAuthKeyRetry in src/auto-reply/reply/agent-runner-execution.ts.
- Caught at line 554, fallback text constructed at line 565:
`⚠️ Agent failed before reply: ${trimmedMessage}.\nLogs: remoteclaw logs --follow`
- Sent to user as the bot's reply.
Root cause
Regression introduced by fc2cefeeab — PR #2360 (chore: sync upstream v2026.3.7 to v2026.3.8 (260 commits)). The sync replaced prior gutted stubs with this new throwing stub:
-// Gutted in RemoteClaw fork (Middleware Boundary Principle)
-export const resolveRunModelFallbacksOverride = (..._args: unknown[]): undefined => undefined;
-export const resolveAgentSkillsFilter = (..._args: unknown[]): undefined => undefined;
+export function resolveAgentRuntimeOrThrow(..._args: unknown[]): never {
+ throw new Error("resolveAgentRuntimeOrThrow is not available in RemoteClaw fork");
+}
The original resolveAgentRuntimeOrThrow (pre-regression) had meaningful semantics — return the configured runtime string, throw only when no runtime configured:
export function resolveAgentRuntimeOrThrow(
cfg: RemoteClawConfig,
agentId: string,
): "claude" | "gemini" | "codex" | "opencode" {
const runtime = resolveAgentRuntime(cfg, agentId);
if (!runtime) {
throw new Error(
"No runtime configured. Set agents.defaults.runtime to one of: claude, gemini, codex, opencode",
);
}
return runtime;
}
The sync regressed this to an unconditional-throw stub. The 3 live callers were kept; the tests were all mocked; CI passed; the bug shipped.
Why tests did not catch it
All 5 call-site test files mock resolveAgentRuntimeOrThrow:
src/cron/isolated-agent/run.auth-retry.test.ts:50
src/cron/isolated-agent/run.channel-bridge.test.ts:71, 464
src/auto-reply/reply/followup-runner.channel-bridge.test.ts:37
src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts:962
Each mock returns "claude" — the correct behavior for the non-stub version. Once this fix restores the real body, all existing mocks remain semantically valid.
Related
Fix
Restore original semantics at src/agents/agent-scope.ts:491-493:
/**
* Resolve per-agent runtime (CLI identifier) — throws if neither per-agent
* config nor defaults define a runtime.
*/
export function resolveAgentRuntimeOrThrow(
cfg: RemoteClawConfig,
agentId: string,
): string {
const runtime = resolveAgentRuntime(cfg, agentId);
if (!runtime) {
throw new Error(
`No runtime configured for agent "${agentId}". Set agents.defaults.runtime to one of: claude, gemini, codex, opencode`,
);
}
return runtime;
}
Return type string (not the literal union) to avoid over-constraining fork-native runtime names; matches resolveAgentRuntime's return shape.
No call-site changes required — the 3 live callers already express the correct contract.
Acceptance criteria
Out of scope
- Broader audit of other throwing-stubs-with-live-callers — tracked separately.
- CI gate to prevent this class of regression — tracked separately.
- LIVE smoke coverage for agent dispatch path — tracked separately (blocked-by this fix).
Summary
src/agents/agent-scope.ts:491-493is an unconditional-throw stub:Three production callers invoke it during every agent dispatch. Every Slack auto-reply, every
/agentCLI invocation, and every cron isolated-agent run crashes with the user-visible:Live callers (unmigrated)
src/auto-reply/reply/agent-runner-execution.tsChannelBridge.providerin auto-reply / Slack pathsrc/commands/agent.tsrunAgentAttemptfor/agentCLIsrc/cron/isolated-agent/run.tsresolvedRuntimein cron isolated agentsrc/cron/isolated-agent/run.tsChannelBridge.providerin cron retry loopThe sibling
resolveAgentRuntime(non-throwing, returnsstring | undefined) exists atsrc/agents/agent-scope.ts:440and works correctly; it's used atsrc/cron/isolated-agent/run.ts:202for telemetry.Error propagation path
resolveAgentRuntimeOrThrow(cfg, agentId)→ throws.withAuthKeyRetryinsrc/auto-reply/reply/agent-runner-execution.ts.Root cause
Regression introduced by
fc2cefeeab— PR #2360 (chore: sync upstream v2026.3.7 to v2026.3.8 (260 commits)). The sync replaced prior gutted stubs with this new throwing stub:The original
resolveAgentRuntimeOrThrow(pre-regression) had meaningful semantics — return the configured runtime string, throw only when no runtime configured:The sync regressed this to an unconditional-throw stub. The 3 live callers were kept; the tests were all mocked; CI passed; the bug shipped.
Why tests did not catch it
All 5 call-site test files mock
resolveAgentRuntimeOrThrow:src/cron/isolated-agent/run.auth-retry.test.ts:50src/cron/isolated-agent/run.channel-bridge.test.ts:71, 464src/auto-reply/reply/followup-runner.channel-bridge.test.ts:37src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts:962Each mock returns
"claude"— the correct behavior for the non-stub version. Once this fix restores the real body, all existing mocks remain semantically valid.Related
Fix
Restore original semantics at
src/agents/agent-scope.ts:491-493:Return type
string(not the literal union) to avoid over-constraining fork-native runtime names; matchesresolveAgentRuntime's return shape.No call-site changes required — the 3 live callers already express the correct contract.
Acceptance criteria
resolveAgentRuntimeOrThrow(cfg, agentId)returns the runtime string whenresolveAgentRuntimeresolves a value.agents.defaults.runtimewhen no runtime configured.pnpm checkpasses (format + typecheck + lint).pnpm testpasses (existing call-site mocks remain valid — they return"claude").LIVE=1 pnpm test:live) reported in PR per project CLAUDE.md §PR Submission Workflow.// ── Upstream-compat stubs (gutted in fork) ───atsrc/agents/agent-scope.ts:437removed or relocated (resolveAgentRuntimeOrThrowis no longer a stub).Out of scope