Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion server/cos-runner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { commandExists } from '../lib/commandExists.js';
import { adoptNpmGlobalBinDir } from '../lib/npmGlobalBin.js';
import { findCommandOnPath } from '../lib/processEnv.js';
import { createCodexStderrFormatter } from '../lib/codexCliOutput.js';
import { isKnownCliStderrNoise } from '../lib/cliStderrNoise.js';
import { createStreamingAnsiStripper } from '../lib/ansiStrip.js';
import { createStreamJsonParser } from './streamJsonParser.js';
import { loadState, saveState, withState } from './runnerState.js';
Expand Down Expand Up @@ -569,7 +570,8 @@ app.post('/spawn', async (req, res) => {
// A chunk that decolors down to whitespace was pure terminal control
// (`opencode run` emits a bare reset per progress redraw). Tagging it
// `[stderr]` would add one blank noise line to the tail per redraw.
if (!decolored.trim()) return;
const trimmedDecolored = decolored.trim();
if (!trimmedDecolored || isKnownCliStderrNoise(trimmedDecolored)) return;
const text = `[stderr] ${decolored}`;
if (agent) agent.outputBuffer += text;
emitToServer('agent:output', { agentId, text });
Expand Down
2 changes: 1 addition & 1 deletion server/cos-runner/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,6 @@ describe('cos-runner output — ANSI decoloring parity with spawnDirectly', () =
});

it('drops a chunk that was only terminal control instead of emitting a blank line', () => {
expect(RUNNER_SRC).toMatch(/if \(!decolored\.trim\(\)\) return;/);
expect(RUNNER_SRC).toMatch(/if \(!trimmedDecolored \|\| isKnownCliStderrNoise\(trimmedDecolored\)\) return;/);
});
});
1 change: 1 addition & 0 deletions server/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ The barrel `server/lib/index.js` is a machine-checkable enumeration of every pub
| `localEndpoint.js` | Dependency-light local-instance URL predicates (`isLocalInstanceHost`, `isLocalInstanceEndpoint`, `localEndpointPort`) shared by provider safety policy and local-runtime classification without importing backend configuration or daemon managers. |
| `cliProviderArgs.js` | Per-CLI argv conventions (`buildCliArgs`) for stdin prompt delivery — dependency-light extraction from runner.js so out-of-process callers (autofixer) can import it. |
| `cliProviderRun.js` | One-shot CLI provider invocation (`pickCliProvider` + `runCliProviderPrompt`) — lightweight path for the autofixer + calendar MCP sync to honor the configured provider/model. |
| `cliStderrNoise.js` | `isKnownCliStderrNoise(trimmedLine)` — drops the real Anthropic `claude` CLI's harmless `[claude-code:unrecognized_model]` SDK telemetry line, which fires on every claude-ollama / claude-ollama-tui run because those providers point the binary's `ANTHROPIC_BASE_URL` at a local Ollama model the SDK doesn't recognize. |
| `codex.js` | OpenAI Codex CLI (`codex`) provider helpers — the `CODEX_COMMAND`/`CODEX_CLI_ID` constants, the `isCodexCommand` predicate, and `ensureCodexTuiArgs` (injects `--dangerously-bypass-approvals-and-sandbox` + disables the startup update-check config, skipped when the argv already declares an approval/sandbox posture via `argvHasFlag`). Extracted from `tuiHandshake.js` (#3618) to match the one-file-per-vendor shape of `antigravity.js`/`grok.js`/`kimi.js`/`cursor.js`, which `providerVendors.js` consumes as registry rows. |
| `cursor.js` | Cursor Agent (`cursor-agent`) provider helpers — the `CURSOR_COMMAND` binary constant, the `isCursorCommand` predicate, and the `ensureCursorHeadlessArgs` (`--print --force`, folding any pinned reasoning effort into `--model`) / `ensureCursorTuiArgs` (`--force`) argv builders. `--force` is load-bearing beyond approvals: it also clears cursor's workspace-trust gate, which otherwise EXITS a headless run before any work happens. The prompt rides raw stdin (like claude/codex, unlike grok/kimi), and cursor needs no configured-default sentinel — its `auto` router is a real model id passed straight to `--model`. |
| `grok.js` | xAI Grok Build (`grok`) provider helpers — id/endpoint constants (`GROK_API_ID`/`GROK_CLI_ID`/`GROK_TUI_ID`/`GROK_API_ENDPOINT`), `isGrokCommand`/`isGrokCliProvider`/`isGrokTuiProvider` predicates, `ensureGrokHeadlessArgs`/`ensureGrokTuiArgs` argv builders (grok reads its prompt from `--prompt-file /dev/stdin`, not raw stdin; model selection uses the `GROK_CONFIGURED_DEFAULT` sentinel in `providerModels.js` so PortOS omits `--model` like Antigravity), and `prepareGrokPromptFile` (Windows temp-file delivery fallback). |
Expand Down
11 changes: 11 additions & 0 deletions server/lib/cliStderrNoise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// The real Anthropic `claude` CLI logs this warning once per unrecognized
// model string whenever its SDK sees a `model` field it doesn't recognize as
// an Anthropic model. PortOS's claude-ollama / claude-ollama-tui providers
// deliberately redirect that binary's ANTHROPIC_BASE_URL at a local Ollama
// endpoint serving non-Anthropic models (e.g. "gemma3:27b"), so this fires on
// every run — it's harmless SDK telemetry, not a misconfiguration or error.
const CLAUDE_SDK_UNRECOGNIZED_MODEL_RE = /^\[claude-code:unrecognized_model\]/;

export function isKnownCliStderrNoise(trimmedLine) {
return CLAUDE_SDK_UNRECOGNIZED_MODEL_RE.test(trimmedLine);
}
12 changes: 12 additions & 0 deletions server/lib/cliStderrNoise.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';
import { isKnownCliStderrNoise } from './cliStderrNoise.js';

describe('isKnownCliStderrNoise', () => {
it('drops the claude CLI SDK unrecognized-model telemetry line', () => {
expect(isKnownCliStderrNoise('[claude-code:unrecognized_model] {"model":"gemma3:27b","query_source":"sdk"}')).toBe(true);
});

it('keeps an unrelated stderr line', () => {
expect(isKnownCliStderrNoise('ECONNREFUSED 127.0.0.1:11434')).toBe(false);
});
});
1 change: 1 addition & 0 deletions server/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export * from './agentExecutionProfiles.js';
export * from './localEndpoint.js';
export * from './cliProviderArgs.js';
export * from './cliProviderRun.js';
export * from './cliStderrNoise.js';
export * from './codex.js';
export * from './codexAccount.js';
export * from './codexTurn.js';
Expand Down
4 changes: 3 additions & 1 deletion server/services/agentCliSpawning.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { normalizeReviewers } from '../lib/validation.js';
import { resolveReviewLoopOptions } from './codeReview.js';
import { safeJSONParse, PATHS, writeFileGuarded } from '../lib/fileUtils.js';
import { createCodexStderrFormatter } from '../lib/codexCliOutput.js';
import { isKnownCliStderrNoise } from '../lib/cliStderrNoise.js';
import { createStreamingAnsiStripper } from '../lib/ansiStrip.js';
import { PROVIDER_TYPES } from '../lib/aiToolkit/constants.js';
import { createImmediateFallbackSignalDetector } from '../lib/aiToolkit/errorDetection.js';
Expand Down Expand Up @@ -686,7 +687,8 @@ export async function spawnDirectly({
// A chunk that decolors down to whitespace was pure terminal control
// (`opencode run` emits a bare reset per progress redraw). Tagging it
// `[stderr]` would add one blank noise line to the tail per redraw.
if (!text.trim()) return;
const trimmed = text.trim();
if (!trimmed || isKnownCliStderrNoise(trimmed)) return;
outputBuffer += `[stderr] ${text}`;
await writeFileGuarded(outputFile, outputBuffer).catch(() => {});
outputBatcher.push(`[stderr] ${text}`);
Expand Down
15 changes: 15 additions & 0 deletions server/services/agentCliSpawning.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,21 @@ describe('stream error containment', () => {
expect(emittedLines().some((line) => line.trim() === '')).toBe(false);
});

it('drops the claude CLI SDK unrecognized-model telemetry line from the surfaced tail', async () => {
const spawnPromise = spawnDirectly(textArgs());
await new Promise((r) => setTimeout(r, 10));

fakeProcess.stderr.emit('data', Buffer.from(
'[claude-code:unrecognized_model] {"model":"gemma3:27b","query_source":"sdk"}\n'
));
await new Promise((r) => setTimeout(r, 30));

fakeProcess.emit('close', 0);
await spawnPromise.catch(() => {});

expect(emittedLines().join('')).not.toMatch(/unrecognized_model/);
});

it('still records a colors-only chunk as run output — it is proof the child is alive', async () => {
// Counting the DECOLORED length would report zero bytes and file a run that
// was steadily redrawing its progress line as having produced nothing.
Expand Down