Skip to content

fix(agents): restore resolveAgentRuntimeOrThrow body — regression stub crashed every agent dispatch #2408

@alexey-pelykh

Description

@alexey-pelykh

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

  1. Caller invokes resolveAgentRuntimeOrThrow(cfg, agentId) → throws.
  2. Error propagates through withAuthKeyRetry in src/auto-reply/reply/agent-runner-execution.ts.
  3. Caught at line 554, fallback text constructed at line 565:
    `⚠️ Agent failed before reply: ${trimmedMessage}.\nLogs: remoteclaw logs --follow`
    
  4. 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

  • resolveAgentRuntimeOrThrow(cfg, agentId) returns the runtime string when resolveAgentRuntime resolves a value.
  • Throws with a clear message naming the agent id and pointing to agents.defaults.runtime when no runtime configured.
  • Regression test asserts both paths (configured → return; missing → throw).
  • pnpm check passes (format + typecheck + lint).
  • pnpm test passes (existing call-site mocks remain valid — they return "claude").
  • LIVE smoke (LIVE=1 pnpm test:live) reported in PR per project CLAUDE.md §PR Submission Workflow.
  • Header comment // ── Upstream-compat stubs (gutted in fork) ─── at src/agents/agent-scope.ts:437 removed or relocated (resolveAgentRuntimeOrThrow is no longer a stub).

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions