Skip to content

tab: enforce deterministic model arg and improve tab-name extraction (opencode)#487

Draft
chr1syy wants to merge 1 commit intoRunMaestro:mainfrom
chr1syy:tab-naming-fix
Draft

tab: enforce deterministic model arg and improve tab-name extraction (opencode)#487
chr1syy wants to merge 1 commit intoRunMaestro:mainfrom
chr1syy:tab-naming-fix

Conversation

@chr1syy
Copy link
Contributor

@chr1syy chr1syy commented Mar 1, 2026

This pull request makes significant improvements to the tab naming logic for agent-based sessions, focusing on more robust and predictable model resolution, enhanced argument sanitization, and improved debugging and logging. The changes ensure that the correct model is used for tab naming, prevent invalid or duplicate CLI arguments, and provide better diagnostics for troubleshooting tab naming quality.

Model Resolution and Argument Handling:

  • Improved the logic for resolving the model ID used in tab naming by strictly preferring (in order): session override, agent config (only if it looks like a valid provider/model), then the agent's default model. The resolved model ID is sanitized to remove trailing slashes and empty values.
  • Enhanced argument sanitization by filtering out non-string arguments, deduplicating --model flags, converting them to a single --model=<value> form, and ensuring only valid model values are passed to the agent CLI. Also, the canonical model flag is injected just before process spawn.

Debugging and Logging Enhancements:

  • Added detailed debug logging throughout the tab naming process, including resolved model information, argument lists before spawning, and final model values. This aids in diagnosing issues with model selection and argument construction. [1] [2] [3]
  • Logged the raw output from the agent before tab name extraction, and added warnings when the agent returns generic or low-quality tab names, helping to identify and address prompt/model issues.

Tab Name Extraction Improvements:

  • Updated the tab name extraction logic to allow quoted single-line outputs and to apply filtering rules to the unquoted value, making extraction more robust to agent output formatting.

Summary by CodeRabbit

  • Bug Fixes

    • Improved tab naming reliability through enhanced model selection logic and argument sanitization.
    • Better handling of remote SSH execution environments with proper model injection.
  • Improvements

    • Enhanced diagnostic logging for troubleshooting tab naming issues.
    • More robust extraction and normalization of tab names from model outputs.
    • Refined configuration resolution for improved accuracy.

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

Enhanced the tab naming handler with stricter model resolution logic (sessionCustomModel > agent-config > default), comprehensive CLI argument sanitization and deduplication, improved SSH remote execution handling, and extensive debug logging throughout the flow. All changes contained within a single file.

Changes

Cohort / File(s) Summary
Model Resolution & CLI Argument Sanitization
src/main/ipc/handlers/tabNaming.ts
Implements priority-based model resolution with validation that config models contain a provider/model separator (/). Sanitizes and normalizes --model CLI flags by removing invalid values, deduplicating multiple flags, and ensuring a canonical --model value is injected when available. Converts multiple --model value pairs into a single --model=value form and strips existing model tokens as needed.
Enhanced Diagnostics & Logging
src/main/ipc/handlers/tabNaming.ts
Adds extensive debug instrumentation for model resolution, config resolution, argument construction, and raw agent output. Logs final arguments before spawning and detects generic tab name responses with warnings. Includes safeguards ensuring finalArgs contains only strings before execution.
SSH Remote Execution & Output Handling
src/main/ipc/handlers/tabNaming.ts
Enhances SSH command handling with environment variable propagation and sanitization, adjusts stdin-based prompting when supported, and applies model injection consistently for remote commands. Extends extractTabName to handle quoted single-line outputs and filters using unquoted content for validation checks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly matches the main changes: enforcing deterministic model argument handling and improving tab-name extraction logic in the tab naming module.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/ipc/handlers/tabNaming.ts (1)

261-262: ⚠️ Potential issue | 🟡 Minor

Non-adjacent includes check may produce false positives.

The condition finalArgs.includes('--input-format') && finalArgs.includes('stream-json') could incorrectly return true if 'stream-json' appears elsewhere in the args (e.g., as a value for a different flag). Consider checking adjacency:

-const hasStreamJsonInput =
-  finalArgs.includes('--input-format') && finalArgs.includes('stream-json');
+const inputFormatIdx = finalArgs.indexOf('--input-format');
+const hasStreamJsonInput =
+  inputFormatIdx !== -1 && finalArgs[inputFormatIdx + 1] === 'stream-json';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 261 - 262, The current
hasStreamJsonInput check can false-positive because it only checks
finalArgs.includes('--input-format') && finalArgs.includes('stream-json');
instead, find the index of '--input-format' in finalArgs (e.g., const idx =
finalArgs.indexOf('--input-format')) and set hasStreamJsonInput = idx !== -1 &&
finalArgs[idx + 1] === 'stream-json' so you verify adjacency; update the code
around the hasStreamJsonInput variable in tabNaming.ts accordingly.
🧹 Nitpick comments (4)
src/main/ipc/handlers/tabNaming.ts (4)

135-152: Type casting suggests missing type definition for sessionCustomModel.

The config parameter is cast to any to access sessionCustomModel, but this property isn't defined in the config type (lines 83-91). If this property is expected, add it to the type definition for type safety.

 async (config: {
   userMessage: string;
   agentType: string;
   cwd: string;
+  sessionCustomModel?: string;
   sessionSshRemoteConfig?: {
     enabled: boolean;
     remoteId: string | null;
     workingDirOverride?: string;
   };
 }): Promise<string | null> => {

Then replace the (config as any).sessionCustomModel casts with direct property access.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 135 - 152, The code uses
(config as any).sessionCustomModel to read sessionCustomModel and bypasses
typing; add sessionCustomModel?: string to the config type/interface used by the
function that declares the config parameter so the property is properly typed,
then remove the casts and access config.sessionCustomModel directly when setting
resolvedModelId (the same pattern should be preserved for resolvedModelId,
agentConfigValues.model and (agent as any).defaultModel — replace the (agent as
any).defaultModel cast by adding defaultModel?: string to the Agent
type/interface or using the existing Agent type so you can use
agent.defaultModel directly).

391-402: Mutating resolvedModelId affects diagnostic logging.

The mutation at line 401 changes resolvedModelId after it was originally resolved. This variable is later used in logging (line 468), which will show the modified value rather than the original resolution. Consider using a separate variable for the final injected model.

+          let finalModelForInjection = resolvedModelId;
           if (
-            !resolvedModelId.includes('/') &&
+            !finalModelForInjection.includes('/') &&
             (agent as any).defaultModel &&
             typeof (agent as any).defaultModel === 'string' &&
             (agent as any).defaultModel.includes('/')
           ) {
-            resolvedModelId = (agent as any).defaultModel as string;
+            finalModelForInjection = (agent as any).defaultModel as string;
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 391 - 402, The code
currently mutates resolvedModelId (in the block that prefers agent.defaultModel
when resolvedModelId lacks a provider prefix), which alters the value later used
for diagnostic logging; instead, introduce a new variable (e.g., injectedModelId
or cliModelId) to hold the model string that will be injected as a CLI flag and
leave resolvedModelId unchanged for logs. Update the injection logic to assign
to injectedModelId when you would have reassigned resolvedModelId and use
injectedModelId for any CLI/config insertion while keeping resolvedModelId for
later logging and diagnostics; reference resolvedModelId, (agent as
any).defaultModel, and the injection site in tabNaming.ts when making the
change.

161-171: Overly defensive try/catch around debug logging.

The try/catch blocks wrapping console.debug calls appear throughout the file (lines 161-171, 190-201, 293-317, etc.). console.debug rarely throws, so this adds noise without meaningful protection.

Consider a helper function if error isolation is truly needed:

const safeDebug = (message: string, data?: object) => {
  try { console.debug(message, data); } catch { /* swallow */ }
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 161 - 171, Remove the
repetitive try/catch wrappers around console.debug in this module and replace
them with a single safeDebug helper; create a top-level const safeDebug = (msg:
string, data?: any) => { try { console.debug(msg, data); } catch { /* swallow */
} } and then change each protected console.debug call (the ones in tabNaming.ts
that currently wrap sessionId/agentType/agentConfigModel/resolvedModelId and
similar debug blocks) to call safeDebug(message, data) instead; this keeps error
isolation but eliminates noisy duplicated try/catch blocks and centralizes
behavior.

319-374: Redundant model flag processing.

The deduplication logic (lines 323-357) extracts the last --model value and consolidates it, then lines 358-374 convert --model <value> to --model=<value>, and finally lines 376-424 strip all model tokens and re-inject the canonical one.

Since lines 376-424 remove all model tokens anyway, the earlier deduplication and conversion steps are effectively discarded. Consider simplifying to just:

  1. Remove all model tokens
  2. Inject the canonical model
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 319 - 374, The earlier
two-step dedupe/convert logic around finalArgs (the inner try using lastModelVal
and the rebuilt conversion into `--model=<value>`) is redundant because later
code already strips model tokens and reinjects a canonical model; remove the
whole deduplication block (the try { ... } that computes lastModelVal and the
subsequent loop that builds rebuilt) and replace it with a single pass before
the prompt separator: compute sepIndex from finalArgs, filter out any `--model`
and `--model=...` tokens in finalArgs.slice(0, sepIndex), then if you have a
canonical model id (use resolvedModelId or the previously determined canonical
variable), push `--model=<canonical>` into the filtered args, and set finalArgs
= [...filteredArgs, ...finalArgs.slice(sepIndex)]; keep the console.debug that
logs sessionId and the canonical value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 261-262: The current hasStreamJsonInput check can false-positive
because it only checks finalArgs.includes('--input-format') &&
finalArgs.includes('stream-json'); instead, find the index of '--input-format'
in finalArgs (e.g., const idx = finalArgs.indexOf('--input-format')) and set
hasStreamJsonInput = idx !== -1 && finalArgs[idx + 1] === 'stream-json' so you
verify adjacency; update the code around the hasStreamJsonInput variable in
tabNaming.ts accordingly.

---

Nitpick comments:
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 135-152: The code uses (config as any).sessionCustomModel to read
sessionCustomModel and bypasses typing; add sessionCustomModel?: string to the
config type/interface used by the function that declares the config parameter so
the property is properly typed, then remove the casts and access
config.sessionCustomModel directly when setting resolvedModelId (the same
pattern should be preserved for resolvedModelId, agentConfigValues.model and
(agent as any).defaultModel — replace the (agent as any).defaultModel cast by
adding defaultModel?: string to the Agent type/interface or using the existing
Agent type so you can use agent.defaultModel directly).
- Around line 391-402: The code currently mutates resolvedModelId (in the block
that prefers agent.defaultModel when resolvedModelId lacks a provider prefix),
which alters the value later used for diagnostic logging; instead, introduce a
new variable (e.g., injectedModelId or cliModelId) to hold the model string that
will be injected as a CLI flag and leave resolvedModelId unchanged for logs.
Update the injection logic to assign to injectedModelId when you would have
reassigned resolvedModelId and use injectedModelId for any CLI/config insertion
while keeping resolvedModelId for later logging and diagnostics; reference
resolvedModelId, (agent as any).defaultModel, and the injection site in
tabNaming.ts when making the change.
- Around line 161-171: Remove the repetitive try/catch wrappers around
console.debug in this module and replace them with a single safeDebug helper;
create a top-level const safeDebug = (msg: string, data?: any) => { try {
console.debug(msg, data); } catch { /* swallow */ } } and then change each
protected console.debug call (the ones in tabNaming.ts that currently wrap
sessionId/agentType/agentConfigModel/resolvedModelId and similar debug blocks)
to call safeDebug(message, data) instead; this keeps error isolation but
eliminates noisy duplicated try/catch blocks and centralizes behavior.
- Around line 319-374: The earlier two-step dedupe/convert logic around
finalArgs (the inner try using lastModelVal and the rebuilt conversion into
`--model=<value>`) is redundant because later code already strips model tokens
and reinjects a canonical model; remove the whole deduplication block (the try {
... } that computes lastModelVal and the subsequent loop that builds rebuilt)
and replace it with a single pass before the prompt separator: compute sepIndex
from finalArgs, filter out any `--model` and `--model=...` tokens in
finalArgs.slice(0, sepIndex), then if you have a canonical model id (use
resolvedModelId or the previously determined canonical variable), push
`--model=<canonical>` into the filtered args, and set finalArgs =
[...filteredArgs, ...finalArgs.slice(sepIndex)]; keep the console.debug that
logs sessionId and the canonical value.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72c43b4 and f958e5b.

📒 Files selected for processing (1)
  • src/main/ipc/handlers/tabNaming.ts

@greptile-apps
Copy link

greptile-apps bot commented Mar 1, 2026

Greptile Summary

This PR refactors the tab-naming model resolution pipeline in tabNaming.ts, adding stricter model ID selection, argument sanitization to remove invalid/duplicate --model flags, and extensive console.debug logging for diagnostics. While the intent is sound, there are three meaningful bugs introduced by the new code.

Key issues found:

  • Model injection after SSH command wrapping (critical): The entire new sanitization and model-injection block (lines 292–424) is placed after the buildSshCommand call (lines 244–290). For SSH sessions, finalArgs at that point already contains the SSH-specific argument structure. Injecting --model=<value> into SSH args corrupts the SSH command — the flag ends up outside the remote agent invocation, not inside it.

  • Silent model loss when model ID lacks a provider prefix: The withoutModel loop unconditionally strips every --model=… and -m token from finalArgs. Re-injection only fires when resolvedModelId.includes('/'). If neither resolvedModelId nor agent.defaultModel contains a /, no model flag is ever re-added, and the spawn silently proceeds without a model argument. The wrapping try/catch swallows all evidence.

  • sessionCustomModel is dead code: The highest-priority model source in the resolution chain reads (config as any).sessionCustomModel, but sessionCustomModel is not present in the config parameter type (lines 83–92) and is never passed by the IPC caller. This field will always be undefined, making that entire code branch unreachable. The described "session override" behavior never executes.

  • resolvedModelId mutation inside a swallowed try/catch: The variable is reassigned at line 401 as a late fallback, but it was already used with its original value in applyAgentConfigOverrides (line 184). The onExit debug log will reflect the post-mutation value, potentially misleading diagnostics.

Confidence Score: 2/5

  • Not safe to merge — the model injection block runs after SSH command wrapping, and the model-stripping logic can silently drop all model flags with no recovery path.
  • Two of the three logic bugs are load-bearing: the SSH ordering issue will corrupt any tab naming request over SSH, and the provider-prefix guard will silently remove model arguments for agents whose model IDs don't use the provider/model format. A third issue (dead-code session override) means a documented feature never runs. These are not hypothetical edge cases — SSH support and non-prefixed model IDs are both in scope for this file.
  • src/main/ipc/handlers/tabNaming.ts — specifically the ordering of the sanitization block relative to buildSshCommand, and the model-injection guard at line 404.

Important Files Changed

Filename Overview
src/main/ipc/handlers/tabNaming.ts Adds model resolution, argument sanitization, and debug logging for tab naming. Contains critical bugs: model injection runs after SSH command wrapping (corrupting SSH args), model flags can be silently stripped with no re-injection when the model ID lacks a provider prefix, and the highest-priority "session override" model path is dead code because sessionCustomModel is not in the config type.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Start: generateTabName] --> B[Fetch agentConfigsStore values]
    B --> C{Resolve resolvedModelId}
    C -->|config.sessionCustomModel?| D["sessionCustomModel branch\n⚠️ Dead code — field not in config type"]
    C -->|agentConfigValues.model with '/'?| E[Use agent config model]
    C -->|agent.defaultModel?| F[Use agent default model]
    D --> G[Sanitize: remove trailing slashes]
    E --> G
    F --> G
    G --> H[buildAgentArgs with modelId]
    H --> I[applyAgentConfigOverrides]
    I --> J[First sanitization: remove invalid --model flags]
    J --> K{SSH enabled?}
    K -->|Yes| L["buildSshCommand → finalArgs = SSH-wrapped args\n⚠️ Model injection happens AFTER this"]
    K -->|No| M[Keep finalArgs as-is]
    L --> N["Final safety sanitization\n(non-string removal)"]
    M --> N
    N --> O["Big sanitization block\n⚠️ Runs on SSH-wrapped args if SSH used"]
    O --> P[Dedup --model flags]
    P --> Q[Convert --model val to --model=val]
    Q --> R[Strip ALL --model= and -m tokens]
    R --> S{"resolvedModelId includes '/'?"}
    S -->|Yes| T[Inject --model=resolvedModelId before '--']
    S -->|No| U["⚠️ No model injected\n(all model flags silently dropped)"]
    T --> V[Spawn process]
    U --> V
Loading

Last reviewed commit: f958e5b

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +292 to +424
// Final safety sanitization: ensure args are all plain strings
try {
const nonStringItems = finalArgs.filter((a) => typeof a !== 'string');
if (nonStringItems.length > 0) {
// eslint-disable-next-line no-console
console.debug('[TabNaming] Removing non-string args before spawn', {
sessionId,
removed: nonStringItems.map((i) => ({ typeof: typeof i, preview: String(i) })),
});
finalArgs = finalArgs.filter((a) => typeof a === 'string');
}

// Extract model arg value for debugging (if present)
const modelIndex = finalArgs.indexOf('--model');
if (modelIndex !== -1 && finalArgs.length > modelIndex + 1) {
const modelVal = finalArgs[modelIndex + 1];
// eslint-disable-next-line no-console
console.debug('[TabNaming] Final --model value', {
sessionId,
value: modelVal,
type: typeof modelVal,
});
}
} catch (err) {
// swallow safety log errors
}

// Quote model values that contain slashes so they survive shell-based
// spawns (PowerShell can interpret unquoted tokens containing slashes).
try {
// Deduplicate --model flags and ensure exactly one is present before the prompt separator
try {
const sepIndex =
finalArgs.indexOf('--') >= 0 ? finalArgs.indexOf('--') : finalArgs.length;
let lastModelVal: string | undefined;
for (let i = 0; i < sepIndex; i++) {
if (finalArgs[i] === '--model' && finalArgs.length > i + 1) {
const cand = finalArgs[i + 1];
if (typeof cand === 'string' && cand.trim()) {
lastModelVal = cand;
}
}
}

if (lastModelVal !== undefined) {
const newArgs: string[] = [];
for (let i = 0; i < sepIndex; i++) {
if (finalArgs[i] === '--model') {
i++; // skip value
continue;
}
newArgs.push(finalArgs[i]);
}
// Insert the single canonical model flag
newArgs.push('--model', lastModelVal);
// Append remaining args (including '--' and prompt)
finalArgs = [...newArgs, ...finalArgs.slice(sepIndex)];
// eslint-disable-next-line no-console
console.debug('[TabNaming] Deduplicated --model flags', {
sessionId,
canonical: lastModelVal,
});
}
} catch (err) {
// ignore dedupe failures
}
// Convert separate --model <value> pairs into a single --model=<value>
// token so shells don't split values. Then enforce a single canonical
// CLI model token derived from our resolvedModelId (if available).
const rebuilt: string[] = [];
for (let i = 0; i < finalArgs.length; i++) {
const a = finalArgs[i];
if (a === '--model' && i + 1 < finalArgs.length) {
const raw = finalArgs[i + 1];
const val =
typeof raw === 'string' ? raw.replace(/^['\"]|['\"]$/g, '') : String(raw);
rebuilt.push(`--model=${val}`);
i++; // skip the value
} else {
rebuilt.push(a);
}
}
finalArgs = rebuilt;

// Remove any existing model tokens (either --model=... or -m/value)
const withoutModel: string[] = [];
for (let i = 0; i < finalArgs.length; i++) {
const a = finalArgs[i];
if (typeof a === 'string' && a.startsWith('--model')) {
// skip
continue;
}
if (a === '-m' && i + 1 < finalArgs.length) {
i++; // skip short form value
continue;
}
withoutModel.push(a);
}

// If we have a resolvedModelId (from session/agent/default), prefer inserting
// it explicitly as a CLI flag to avoid relying on OpenCode config/env.
if (resolvedModelId && typeof resolvedModelId === 'string') {
// If resolvedModelId doesn't look like provider/model, prefer agent.defaultModel
if (
!resolvedModelId.includes('/') &&
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string' &&
(agent as any).defaultModel.includes('/')
) {
resolvedModelId = (agent as any).defaultModel as string;
}

if (resolvedModelId && resolvedModelId.includes('/')) {
const modelToken = `--model=${resolvedModelId}`;
// Insert before the argument separator `--` if present
const sep = withoutModel.indexOf('--');
if (sep === -1) {
withoutModel.push(modelToken);
} else {
withoutModel.splice(sep, 0, modelToken);
}
// eslint-disable-next-line no-console
console.debug('[TabNaming] Injected canonical --model for spawn', {
sessionId,
model: resolvedModelId,
});
}
}

finalArgs = withoutModel;
} catch (err) {
// swallow
}
Copy link

Choose a reason for hiding this comment

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

Model injection runs after SSH command wrapping

The entire new sanitization and model-injection block (lines 292–424) executes after the SSH command is constructed (lines 244–290). When SSH is enabled, finalArgs at this point already contains the SSH-specific argument list (i.e., the remote agent command has been embedded as a single shell string or as SSH flags). The model-injection logic then scans those SSH args for -- separators and injects --model=<value> into the wrong position, corrupting the SSH command structure.

To fix this, the model injection must happen before buildSshCommand is called, so the model flag ends up inside the remote agent invocation, not in the SSH wrapper:

// ✅ Inject model flag here, before SSH wrapping
//    ... (sanitize finalArgs)

if (config.sessionSshRemoteConfig?.enabled && ...) {
    const sshCommand = await buildSshCommand(...)
    command = sshCommand.command;
    finalArgs = sshCommand.args;
}

// ❌ Do NOT modify finalArgs here for SSH sessions

Comment on lines +376 to +421
// Remove any existing model tokens (either --model=... or -m/value)
const withoutModel: string[] = [];
for (let i = 0; i < finalArgs.length; i++) {
const a = finalArgs[i];
if (typeof a === 'string' && a.startsWith('--model')) {
// skip
continue;
}
if (a === '-m' && i + 1 < finalArgs.length) {
i++; // skip short form value
continue;
}
withoutModel.push(a);
}

// If we have a resolvedModelId (from session/agent/default), prefer inserting
// it explicitly as a CLI flag to avoid relying on OpenCode config/env.
if (resolvedModelId && typeof resolvedModelId === 'string') {
// If resolvedModelId doesn't look like provider/model, prefer agent.defaultModel
if (
!resolvedModelId.includes('/') &&
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string' &&
(agent as any).defaultModel.includes('/')
) {
resolvedModelId = (agent as any).defaultModel as string;
}

if (resolvedModelId && resolvedModelId.includes('/')) {
const modelToken = `--model=${resolvedModelId}`;
// Insert before the argument separator `--` if present
const sep = withoutModel.indexOf('--');
if (sep === -1) {
withoutModel.push(modelToken);
} else {
withoutModel.splice(sep, 0, modelToken);
}
// eslint-disable-next-line no-console
console.debug('[TabNaming] Injected canonical --model for spawn', {
sessionId,
model: resolvedModelId,
});
}
}

finalArgs = withoutModel;
Copy link

Choose a reason for hiding this comment

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

All model flags stripped but not re-injected when model lacks a provider prefix

The withoutModel loop unconditionally removes every --model=… and -m token from finalArgs (lines 376–389). The model is only re-injected at line 405 when resolvedModelId.includes('/'). If resolvedModelId was resolved from agent.defaultModel and that string has no / component (e.g. just a model short-name), neither the fallback at line 395 nor the injection at line 404 fires, and the spawn happens with no model argument at all — silently, because the outer try/catch swallows everything.

Trace for a model without a provider prefix:

  1. resolvedModelId = "my-model" after initial resolution
  2. Line 395: !resolvedModelId.includes('/') is true, but if agent.defaultModel also lacks /, the inner if is not entered and resolvedModelId is unchanged
  3. Line 404: resolvedModelId.includes('/') is false → no token injected
  4. finalArgs = withoutModel — all previous model flags stripped, nothing added back

The agent CLI then runs with whatever its built-in default model is, silently diverging from the configured value. The fix is to inject the model token regardless of whether it contains a /, or at minimum, avoid stripping model tokens that cannot be re-injected.

Comment on lines +134 to +152
let resolvedModelId: string | undefined;
if (
typeof (config as any).sessionCustomModel === 'string' &&
(config as any).sessionCustomModel.trim()
) {
resolvedModelId = (config as any).sessionCustomModel.trim();
} else if (
agentConfigValues &&
typeof agentConfigValues.model === 'string' &&
agentConfigValues.model.trim() &&
agentConfigValues.model.includes('/')
) {
resolvedModelId = agentConfigValues.model.trim();
} else if (
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string'
) {
resolvedModelId = (agent as any).defaultModel as string;
}
Copy link

Choose a reason for hiding this comment

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

sessionCustomModel branch is dead code — field is not in the handler config type

The IPC handler's config parameter (lines 83–92) is typed as:

config: {
  userMessage: string;
  agentType: string;
  cwd: string;
  sessionSshRemoteConfig?: { ... };
}

sessionCustomModel is not part of this type. Accessing it via (config as any).sessionCustomModel will always evaluate to undefined, so the first branch of the model-resolution chain (lines 135–139) is never entered. The PR description lists "session override" as the highest-priority model source, but it will never be applied until sessionCustomModel is added to both the config type and the IPC caller.

Suggested change
let resolvedModelId: string | undefined;
if (
typeof (config as any).sessionCustomModel === 'string' &&
(config as any).sessionCustomModel.trim()
) {
resolvedModelId = (config as any).sessionCustomModel.trim();
} else if (
agentConfigValues &&
typeof agentConfigValues.model === 'string' &&
agentConfigValues.model.trim() &&
agentConfigValues.model.includes('/')
) {
resolvedModelId = agentConfigValues.model.trim();
} else if (
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string'
) {
resolvedModelId = (agent as any).defaultModel as string;
}
let resolvedModelId: string | undefined;
if (
typeof (config as any).sessionCustomModel === 'string' &&
(config as any).sessionCustomModel.trim()
) {
resolvedModelId = (config as any).sessionCustomModel.trim();
} else if (

Consider adding sessionCustomModel?: string to the config interface and threading the value through from the caller, or removing this branch if it is not yet implemented.

Comment on lines +393 to +402
if (resolvedModelId && typeof resolvedModelId === 'string') {
// If resolvedModelId doesn't look like provider/model, prefer agent.defaultModel
if (
!resolvedModelId.includes('/') &&
(agent as any).defaultModel &&
typeof (agent as any).defaultModel === 'string' &&
(agent as any).defaultModel.includes('/')
) {
resolvedModelId = (agent as any).defaultModel as string;
}
Copy link

Choose a reason for hiding this comment

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

resolvedModelId mutation inside a swallowed try/catch causes misleading debug logs

resolvedModelId is mutated at line 401 inside the outer try/catch whose errors are silently swallowed (line 422–424). This variable is captured by reference in the onExit closure (line 464) and will reflect the post-mutation value when logged after process exit — even if the mutation happened in a code path that was not supposed to be the "winning" resolution.

More importantly, resolvedModelId was already used as sessionCustomModel in applyAgentConfigOverrides (line 184) with the original value. The late mutation only affects the final injection (line 405) and the debug logs, creating an inconsistency between what was passed to applyAgentConfigOverrides and what is ultimately injected/logged.

Consider using a separate variable (e.g. finalModelId) for this late fallback so the original resolvedModelId remains stable for logging purposes.

@chr1syy chr1syy marked this pull request as draft March 1, 2026 10:58
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.

1 participant