Skip to content

fix(acpx): add windowsHide to MCP proxy spawn on Windows#60678

Open
KChow-ctrl wants to merge 2 commits intoopenclaw:mainfrom
KChow-ctrl:fix/issue-60672
Open

fix(acpx): add windowsHide to MCP proxy spawn on Windows#60678
KChow-ctrl wants to merge 2 commits intoopenclaw:mainfrom
KChow-ctrl:fix/issue-60672

Conversation

@KChow-ctrl
Copy link
Copy Markdown

@KChow-ctrl KChow-ctrl commented Apr 4, 2026

Fixes #60672.

Summary

  • Set windowsHide: true for the acpx MCP proxy target process on Windows.
  • Keep non-Windows spawn options unchanged.
  • Drop the unrelated Feishu and Ollama changes from the original branch.
  • Add focused spawn-option coverage and a changelog entry.

Verification

  • pnpm install --frozen-lockfile
  • pnpm test extensions/acpx/src/runtime-internals/mcp-proxy.test.ts extensions/acpx/src/runtime-internals/mcp-command-line.test.ts — 6 tests passed
  • pnpm exec oxfmt --check --threads=1 CHANGELOG.md extensions/acpx/src/runtime-internals/mcp-proxy.mjs extensions/acpx/src/runtime-internals/mcp-proxy.test.ts
  • git diff --check
  • pnpm check:changed

Notes

pnpm test:extensions acpx passed the acpx shard itself (71 tests) but then continued iterating unrelated extension configs with no matching acpx test files; I stopped that broad wrapper after the acpx shard completed and used the focused acpx tests plus check:changed as the gate.

Live Windows ACP proof was not run from this macOS maintainer workspace; the issue is source-backed and covered by focused regression tests. A proof: override label/comment records that maintainer decision.

@KChow-ctrl
Copy link
Copy Markdown
Author

This PR fixes issue #60672. The MCP proxy was not setting windowsHide: true when spawning the target agent command on Windows, which caused the spawned process to exit with code 1 due to terminal window handling issues.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12277b984f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/feishu/src/media.ts Outdated
Comment on lines +541 to +544
ext === ".mp3" ||
contentType === "audio/ogg" ||
contentType === "audio/opus"
contentType === "audio/opus" ||
contentType === "audio/mpeg"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep MP3 files out of the Opus upload path

This change routes .mp3/audio/mpeg through resolveFeishuOutboundMediaKind as fileType: "opus" + msgType: "audio", but the send path uploads the original bytes directly (no MP3→Opus transcoding before im.file.create). In practice, MP3 media will be mislabeled as Opus, which can cause Feishu to reject the upload or produce broken audio playback; the previous stream/file fallback avoided that mismatch.

Useful? React with 👍 / 👎.

Comment thread extensions/ollama/src/stream.ts Outdated
Comment on lines +731 to +733
// Clean up timeout if we set one
if (timeoutId) {
clearTimeout(timeoutId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear request timeout when fetch errors

The timer is cleared only after await fetch(...) returns successfully, so connection-level failures (for example DNS/connect/TLS errors) skip this cleanup and leave the timeout pending until it fires. Under repeated failures this accumulates unnecessary timers and can keep the process/event loop alive longer than intended; clearTimeout should run from a finally path that executes on both success and error.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 4, 2026

Greptile Summary

This PR bundles four independent fixes despite a title and description that only describe one of them (the acpx windowsHide change). The individual fixes are:

  1. extensions/acpx/src/runtime-internals/mcp-proxy.mjs — Adds windowsHide: true to the MCP-proxy child-process spawn on Windows, matching the existing behavior of the main acpx spawn. Correct and minimal.

  2. extensions/ollama/src/stream.ts + extensions/ollama/index.ts — Adds a configurable per-request timeout (defaulting to 2 minutes) to the native Ollama stream function, wired through from timeoutSeconds in the provider config. Also inlines the previously-external parseJsonPreservingUnsafeIntegers logic and removes the normalizeOllamaCompatMessageToolArgs helper in favour of tighter Record<string, unknown> types at the call site.

  3. extensions/feishu/src/media.ts — Maps .mp3/audio/mpeg to Feishu's file_type: \"opus\" / msg_type: \"audio\" so TTS audio is delivered as a voice message rather than a file attachment. Also consolidates the withTempDownloadPath import into the local runtime-api.js barrel (an import-boundary improvement).

Notable concerns:

  • The PR title and first commit message only describe the acpx change; the other three commits carry unrelated fixes. Future bisects and changelogs will be harder to follow.
  • The Feishu change uploads MP3 data with file_type: \"opus\", which may cause Feishu to reject the upload or produce a broken audio message if the API validates codec format against the declared type.
  • The new Ollama timeout timer is not cleared in error/abort code paths (minor resource concern).

Confidence Score: 3/5

Safe to merge for the acpx and Ollama timeout changes; the Feishu MP3→opus type mismatch warrants verification before shipping.

The core acpx fix is straightforward and low-risk. The Ollama timeout is a sensible addition. The Feishu change is the main uncertainty: uploading MP3 bytes with file_type: opus may fail on the Feishu side if the API validates codec format, which would be a user-visible regression for audio messages. If the author has confirmed this works end-to-end on Feishu, confidence rises to 4–5.

extensions/feishu/src/media.ts — the MP3-as-opus upload type and its compatibility with the Feishu API needs verification. extensions/ollama/src/stream.ts — minor: timeout cleanup on error path.

Comments Outside Diff (1)

  1. extensions/feishu/src/media.ts, line 538-548 (link)

    P2 MP3 uploaded with fileType: "opus" — potential Feishu API mismatch

    .mp3 files (and audio/mpeg content) are routed to fileType: "opus" and msgType: "audio". The Feishu im.file.create API uses file_type both for routing and, for audio, typically expects an Opus-encoded file when "opus" is declared. Sending a raw MP3 binary under that type may work if Feishu's backend is lenient, but it could also silently produce a broken audio message or return an API error.

    If the intent is to deliver TTS MP3 audio as a voice message, consider either:

    • Re-encoding to Opus before upload (the canonical approach), or
    • Using file_type: "stream" with msg_type: "file" as a safe fallback until encoding is in place.

    If you've tested this end-to-end on Feishu and confirmed it works, a brief comment here would help future readers understand why MP3 is declared as "opus".

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/feishu/src/media.ts
    Line: 538-548
    
    Comment:
    **MP3 uploaded with `fileType: "opus"` — potential Feishu API mismatch**
    
    `.mp3` files (and `audio/mpeg` content) are routed to `fileType: "opus"` and `msgType: "audio"`. The Feishu `im.file.create` API uses `file_type` both for routing and, for audio, typically expects an Opus-encoded file when `"opus"` is declared. Sending a raw MP3 binary under that type may work if Feishu's backend is lenient, but it could also silently produce a broken audio message or return an API error.
    
    If the intent is to deliver TTS MP3 audio as a voice message, consider either:
    - Re-encoding to Opus before upload (the canonical approach), or
    - Using `file_type: "stream"` with `msg_type: "file"` as a safe fallback until encoding is in place.
    
    If you've tested this end-to-end on Feishu and confirmed it works, a brief comment here would help future readers understand why MP3 is declared as `"opus"`.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/feishu/src/media.ts
Line: 538-548

Comment:
**MP3 uploaded with `fileType: "opus"` — potential Feishu API mismatch**

`.mp3` files (and `audio/mpeg` content) are routed to `fileType: "opus"` and `msgType: "audio"`. The Feishu `im.file.create` API uses `file_type` both for routing and, for audio, typically expects an Opus-encoded file when `"opus"` is declared. Sending a raw MP3 binary under that type may work if Feishu's backend is lenient, but it could also silently produce a broken audio message or return an API error.

If the intent is to deliver TTS MP3 audio as a voice message, consider either:
- Re-encoding to Opus before upload (the canonical approach), or
- Using `file_type: "stream"` with `msg_type: "file"` as a safe fallback until encoding is in place.

If you've tested this end-to-end on Feishu and confirmed it works, a brief comment here would help future readers understand why MP3 is declared as `"opus"`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/ollama/src/stream.ts
Line: 711-738

Comment:
**Timeout timer not cancelled on fetch error path**

`clearTimeout(timeoutId)` is only reached when `fetch` returns without throwing. If the fetch is aborted (by the timer itself or another error), execution jumps to the `catch` block and the timer is never explicitly cancelled. In the timeout-fires case this is a no-op (the timer already ran), but it means the `clearTimeout` call is missing for non-abort errors while the timer is still pending — a minor resource leak.

Consider placing `if (timeoutId) clearTimeout(timeoutId);` inside the outer `finally` block that already calls `stream.end()`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(acpx): add windowsHide to MCP proxy ..." | Re-trigger Greptile

Comment thread extensions/ollama/src/stream.ts Outdated
Comment on lines 711 to 738
headers.Authorization = `Bearer ${options.apiKey}`;
}

// Apply request timeout if configured and no external signal is provided.
// Local Ollama may need longer timeout than the default agent timeout.
let finalSignal: AbortSignal | undefined = options?.signal;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
if (!finalSignal && defaultTimeoutMs && defaultTimeoutMs > 0) {
const timeoutController = new AbortController();
timeoutId = setTimeout(() => timeoutController.abort(), defaultTimeoutMs);
finalSignal = timeoutController.signal;
}

const response = await fetch(chatUrl, {
method: "POST",
headers,
body: JSON.stringify(body),
signal: options?.signal,
signal: finalSignal,
});

// Clean up timeout if we set one
if (timeoutId) {
clearTimeout(timeoutId);
}

if (!response.ok) {
const errorText = await response.text().catch(() => "unknown error");
throw new Error(`${response.status} ${errorText}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Timeout timer not cancelled on fetch error path

clearTimeout(timeoutId) is only reached when fetch returns without throwing. If the fetch is aborted (by the timer itself or another error), execution jumps to the catch block and the timer is never explicitly cancelled. In the timeout-fires case this is a no-op (the timer already ran), but it means the clearTimeout call is missing for non-abort errors while the timer is still pending — a minor resource leak.

Consider placing if (timeoutId) clearTimeout(timeoutId); inside the outer finally block that already calls stream.end().

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/ollama/src/stream.ts
Line: 711-738

Comment:
**Timeout timer not cancelled on fetch error path**

`clearTimeout(timeoutId)` is only reached when `fetch` returns without throwing. If the fetch is aborted (by the timer itself or another error), execution jumps to the `catch` block and the timer is never explicitly cancelled. In the timeout-fires case this is a no-op (the timer already ran), but it means the `clearTimeout` call is missing for non-abort errors while the timer is still pending — a minor resource leak.

Consider placing `if (timeoutId) clearTimeout(timeoutId);` inside the outer `finally` block that already calls `stream.end()`.

How can I resolve this? If you propose a fix, please make it concise.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 1, 2026

Codex review: needs maintainer review before merge.

Summary
The PR adds a Windows-only windowsHide spawn option for the ACPX MCP proxy target process, focused regression coverage, and a changelog entry.

Reproducibility: yes. source-reproducible: #60672 gives concrete Windows ACP Codex steps, and current main still spawns the MCP proxy target without windowsHide. I did not run a live Windows ACP session in this read-only review.

Real behavior proof
Override: Override: the PR carries proof: override, and the maintainer refresh comment records that live Windows ACP proof was not run but source and focused test evidence are accepted for this branch.

Next step before merge
The patch is narrow and correct-looking, but exact-head CI has a check-additional architecture failure without a clear automated file-level repair from the available annotations.

Security
Cleared: Cleared: the live diff changes an existing child-process spawn option, focused test coverage, and changelog text without adding dependency, workflow, secret, or package-resolution surface.

Review details

Best possible solution:

Land the narrowed ACPX fix after the exact-head architecture/check-additional failure is triaged or rerun green; keep broader Windows spawn cleanup such as #48320 separate.

Do we have a high-confidence way to reproduce the issue?

Yes, source-reproducible: #60672 gives concrete Windows ACP Codex steps, and current main still spawns the MCP proxy target without windowsHide. I did not run a live Windows ACP session in this read-only review.

Is this the best way to solve the issue?

Yes. Adding windowsHide: true only to the Windows MCP proxy target-spawn options is the narrowest maintainable fix and keeps non-Windows spawn behavior unchanged.

What I checked:

Likely related people:

  • steipete: Current-main blame for the ACPX MCP proxy file points to Peter Steinberger in this checkout, and the provided review history identifies him as landing the MCP bootstrap and adjacent Windows wrapper hardening. (role: merged feature history and adjacent Windows spawn maintainer; confidence: high; commits: eabae023ebb2, 5659d7f985eb, 68a8a98ab761; files: extensions/acpx/src/runtime-internals/mcp-proxy.mjs, extensions/acpx/src/runtime-internals/mcp-proxy.test.ts, src/plugin-sdk/windows-spawn.ts)
  • goodspeed-apps: The provided prior ClawSweeper review says the landed MCP bootstrap commit explicitly credited this contributor for the original ACPX MCP bootstrap work that includes the proxy path under review. (role: introduced behavior; confidence: medium; commits: 5659d7f985eb; files: extensions/acpx/src/runtime-internals/mcp-proxy.mjs, extensions/acpx/src/runtime-internals/mcp-proxy.test.ts)
  • BradGroux: BradGroux force-pushed the narrowed acpx-only branch, added the proof override, moved the changelog entry, and recorded the latest maintainer verification for this PR. (role: likely follow-up owner; confidence: medium; commits: d9931ff607c8, 9cf5c1ab07a8; files: CHANGELOG.md, extensions/acpx/src/runtime-internals/mcp-proxy.mjs, extensions/acpx/src/runtime-internals/mcp-proxy.test.ts)

Remaining risk / open question:

  • Exact-head CI is not fully green: check-additional-runtime-topology-architecture currently reports check:architecture failed, and the available public annotations do not expose a narrow line-level cause.
  • No live Windows ACP run output is attached; contributor action is not required because the PR has a maintainer proof: override, but runtime confidence still rests on source inspection and focused tests.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 4fe12fc0998c.

Re-review progress:

@openclaw-barnacle openclaw-barnacle Bot removed the channel: feishu Channel integration: feishu label May 6, 2026
@BradGroux BradGroux added the proof: override Maintainer override for the external PR real behavior proof gate. label May 6, 2026
@BradGroux
Copy link
Copy Markdown
Member

Maintainer prep update:

  • Rebased onto current origin/main.
  • Narrowed the branch to the acpx MCP proxy Windows windowsHide fix only; dropped unrelated Feishu/Ollama changes from the original branch.
  • Added focused spawn-option coverage and changelog.
  • Prepared head: 132e71ff1385b5e2a635b65e2e0deb26b94b0bd5.
  • Added proof: override because this macOS maintainer workspace cannot produce live Windows ACP output, but the source gap is clear and the option behavior is covered by focused tests.

Verification run locally:

  • pnpm install --frozen-lockfile
  • pnpm test extensions/acpx/src/runtime-internals/mcp-proxy.test.ts extensions/acpx/src/runtime-internals/mcp-command-line.test.ts — 6 tests passed
  • pnpm exec oxfmt --check --threads=1 CHANGELOG.md extensions/acpx/src/runtime-internals/mcp-proxy.mjs extensions/acpx/src/runtime-internals/mcp-proxy.test.ts — passed
  • git diff --check — passed
  • pnpm check:changed — passed

Note: pnpm test:extensions acpx passed the acpx shard itself (71 tests) but continued walking unrelated extension configs with the acpx filter; I stopped that broad wrapper after the acpx shard completed and relied on the focused acpx tests plus check:changed.

@BradGroux
Copy link
Copy Markdown
Member

Maintainer refresh: rebased the prepared branch onto current main after main moved.\n\nNew prepared head: 974fd3bdf74752da73f671e4848e8e0a5e5ea976\nPrevious prepared head: 132e71ff1385b5e2a635b65e2e0deb26b94b0bd5\n\nFocused verification after refresh:\n- pnpm test extensions/acpx/src/runtime-internals/mcp-proxy.test.ts extensions/acpx/src/runtime-internals/mcp-command-line.test.ts passed (2 files, 6 tests)\n- pnpm exec oxfmt --check --threads=1 CHANGELOG.md extensions/acpx/src/runtime-internals/mcp-proxy.mjs extensions/acpx/src/runtime-internals/mcp-proxy.test.ts passed\n- git diff --check passed\n\nFresh CI is running on the refreshed head.

@BradGroux
Copy link
Copy Markdown
Member

Maintainer prep refresh for #60678.

Prepared head: 9cf5c1ab07a89c7f11f2f3e6ed763cc2dd25664a

What I changed:

  • Rebased onto current origin/main.
  • Moved the existing changelog entry to the SOP-required tail position for the current section.

Verification:

  • pnpm vitest run extensions/acpx/src/runtime-internals/mcp-proxy.test.ts passed (2 tests).
  • pnpm build passed.
  • pnpm check passed.
  • Full pnpm test was attempted and is currently blocked by unrelated mainline shard failures outside this PR's ACPX path, including config patch/model fallback/fs-safe staging expectations and extension messaging DNS (api.example.test) failures.

Fresh CI is running on the prepared head now. Not merging until required/full checks are green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extensions: acpx proof: override Maintainer override for the external PR real behavior proof gate. size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Windows ACP Codex spawn via OpenClaw exits with code 1 while direct acpx codex initializes

2 participants