Skip to content

fix: skip response body cloning in logResult when debug logging is disabled - #8

Open
peeyush-ciridae wants to merge 1 commit into
dataforxyz:mainfrom
peeyush-ciridae:fix/logresult-clone-crash
Open

peeyush-ciridae wants to merge 1 commit into
dataforxyz:mainfrom
peeyush-ciridae:fix/logresult-clone-crash

Conversation

@peeyush-ciridae

Copy link
Copy Markdown

Problem

logResult() unconditionally clones and reads result.response before handing it to logInject(), which is itself already a no-op unless OPENCODE_INTERCOM_DEBUG=1:

async function logResult(step, result, details = {}) {
  const responseBody = result.response
    ? await result.response.clone().text().catch(() => undefined)
    : undefined;
  logInject(step, { ...details, ... });
}

By the time client.session.list(), client.tui.appendPrompt(), client.tui.submitPrompt(), etc. resolve, the SDK has already consumed the underlying response body while parsing .data. Calling .clone() on an already-consumed Response throws synchronously:

TypeError: Body is disturbed or locked
 code: "ERR_BODY_ALREADY_USED"

The trailing .catch() is attached to .clone().text(), so it doesn't catch a synchronous throw from .clone() itself. This propagates out of logResult uncaught.

Impact

Two concrete, user-facing consequences of the same root cause:

  1. resolveActiveSessionID() — called once at boot inside the top-level connect() IIFE. The exception aborts the function before setActiveSession() runs, so this always fails, logging "Failed to start OpenCode intercom listener" on every single plugin start (regardless of OPENCODE_INTERCOM_DEBUG).

  2. injectInbound() — in a TTY session, client.tui.appendPrompt() succeeds (the inbound message text is appended to the user's prompt input), but the very next line, await logResult("inject.append", ...), throws before the if (appended.data === true) check that guards client.tui.submitPrompt(). The catch block only calls the debug-gated logInject(), so the failure is silent — submitPrompt() is never invoked. The inbound message sits in the prompt box requiring the user to manually press Enter, instead of being auto-submitted.

Fix

Guard logResult() the same way logInject() already guards itself, so the unnecessary response clone/read (and its crash) never happens when debug logging is off:

async function logResult(step, result, details = {}) {
  if (!debugInject) {
    return;
  }
  const responseBody = result.response
    ? await result.response.clone().text().catch(() => undefined)
    : undefined;
  ...
}

Minimal, single call site, matches the existing logInject early-return pattern already in the file.

Testing

  • npm run build — regenerated dist/plugin.mjs and dist/index.mjs from source via the project's own scripts/build.mjs.
  • npm run typecheck — clean, no errors.
  • npm test — 140/143 pass. The 3 failures (broker/remote-access.integration.test.ts, broker/session-collision.integration.test.ts) reproduce identically on unpatched main in this sandbox (child-process broker spawn failing, "broker exited early: 1") — pre-existing environment issue unrelated to this change, verified by stashing the fix and re-running the same files.
  • Manually verified against the published 0.10.0 build (before rebasing this fix onto source): applying the equivalent one-line guard to the installed dist/plugin.mjs eliminated the "Failed to start OpenCode intercom listener" crash on every opencode run invocation.

Notes

Verifying the injectInboundsubmitPrompt fix end-to-end requires a live inbound Intercom message in a TTY session; I confirmed the mechanism precisely from source (this PR's description above) and confirmed the boot-time crash is eliminated, but haven't captured a recording of the TUI auto-submit specifically.

…sabled

logResult() unconditionally cloned and read result.response before
passing it to logInject(), which itself is a no-op unless
OPENCODE_INTERCOM_DEBUG=1. By the time client.session.list(),
client.tui.appendPrompt(), etc. return, the SDK has already consumed
the underlying response body, so .clone() throws
'TypeError: Body is disturbed or locked' (ERR_BODY_ALREADY_USED).

This throw is uncaught at two call sites with concrete user-facing
impact:

- resolveActiveSessionID(): the exception aborts the function before
  setActiveSession() runs, so the boot-time session resolution in the
  top-level connect() IIFE always fails, logging 'Failed to start
  OpenCode intercom listener' on every plugin start.

- injectInbound(): in a TTY session, client.tui.appendPrompt()
  succeeds (the inbound message text is appended to the prompt input),
  but the immediately following 'await logResult("inject.append", ...)'
  throws before the 'if (appended.data === true)' check that guards
  client.tui.submitPrompt(). The catch block only calls the
  debug-gated logInject(), so the failure is silent and submitPrompt()
  is never invoked -- the message sits in the input box requiring a
  manual Enter instead of being submitted automatically.

Guard logResult() the same way logInject() already guards itself, so
the unnecessary response clone/read never runs when debug logging is
off.

Verified: dist rebuilt via 'npm run build', 'npm run typecheck' clean,
'npm test' unaffected (3 pre-existing broker/*.integration.test.ts
failures reproduce identically on unpatched main; unrelated to this
change).
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