fix(coding-agent): extension failures render as a calm notice; stacks go to the debug log (#4990)

CO
2d ago
+961-178

Part of #4990

What changed

An extension handler failure used to reach the TUI as Extension "<absolute path>" error: <message> followed by every stack frame (URL-encoded senpi-extension: module frames, node:events, processTicksAndRejections). Every surface now builds one notice from the failure, and no surface renders the stack:

  • Contract (core/extensions/user-facing-error.ts, exported from the package): UserFacingError / userFacingErrorOf / UserFacingErrorInfo = code + message + optional action + optional http(s) docsUrl. Recognition is structural (an own userFacing property), so a bundled extension opts in without importing senpi. Malformed contracts fall back to the unexpected path.
  • One formatter (core/extensions/error-notice.ts): coded failure -> title = what happened, detail = what to do, footer <extension> · <CODE>. Anything else -> Something went wrong in <extension>, the first line of the message with file paths and senpi-extension: module URLs cut to file names (240 chars max), (during <event>), and Details: <debug log>.
  • Stacks to the debug log: ExtensionRunner.emitError records every failure (message + stack, through the existing redacting appendDebugLogEntry). All runner and context-dispatch capture sites (14) now go through extensionFailureFields, which also carries userFacing.
  • Surfaces: TUI showExtensionError shows a warning-tone notice box (sanitized, as before); print mode writes the same lines to stderr; RPC extension_error keeps error for existing clients and adds a stack-free notice; startup diagnostics (provider / virtual model registration) and project_trust failures use the one-line form; senpi package commands no longer print a settings error's stack.
  • Docs: docs/extensions.md (Error Handling + example), docs/rpc.md (extension_error.notice).

Before -> after for the report in #4990:

Extension "~/.omo/omo-versions/<v>/.../omo.js" error: COMPUTER_LAUNCH_CONTEXT_REQUIRED: ...
   at finish (senpi-extension:0/...omo-computer-use.js:178:40)
   ...
 Computer use cannot start safely from this host's current macOS launch context.
 Restart this host from a local terminal and keep that terminal app running, or use the desktop app (Settings > Restart engine).
 computer · COMPUTER_LAUNCH_CONTEXT_REQUIRED

Tests

  • test/suite/regressions/issue-4990-calm-extension-error-notice.test.ts: real loader + runner + InteractiveMode renderer. RED on main (rendered the absolute path headline and full stack; debug log absent), GREEN here: coded and uncoded failures render with no stack frame, no temp path, no senpi-extension:, no Extension " framing; inline snapshot of the calm notice; the stack is in the debug log.
  • test/extension-error-notice.test.ts: contract parsing (class + structural, rejects malformed, http(s)-only docs), source names, first-line + path cutting (POSIX, Windows, module URLs, /login untouched), control-sequence stripping (the assertions of the removed extension-error-format.test.ts), RPC record shape.
  • Neighbours green: extensions-runner, agent-session-services-providers, extension-load-diagnostics, print-mode, interactive-mode-status, 8423/9068 regressions (163 tests).
  • bun run check rc=0.

QA and the whole-suite receipt follow in comments.


Summary by cubic

Extension handler failures now render as a calm notice instead of a crash dump. The old Extension "<path>" error: headline and stack frames are gone, replaced by the same notice across TUI, print mode, RPC, and startup diagnostics. Stacks go to the debug log; no interface shows them.

  • A new UserFacingError contract (recognized structurally via a userFacing property, so bundled extensions opt in without importing senpi) lets a failure carry a code, message, optional action, and docs URL. Everything else shows "Something went wrong in <extension>" with the first line, paths cut to file names, and the log location.
  • Every failure site routes through one helper, including deferred (ctx.afterSessionStart) calls, session moves, and session-control reload. Thrown values are read through guarded property reads, so a throwing getter or Proxy falls back to the unexpected path instead of escaping handler isolation.
  • Uncoded reasons strip control and terminal sequences, cut quoted, UNC, and spaced home-directory paths to file names, cap at 240 chars without splitting surrogate pairs, and name slash-command sources /<command>; the Windows debug-log path shortens to ~.
  • RPC extension_error gains a stack-free notice; error stays for existing clients. Docs and changelog updated, with regression tests for coded and uncoded failures.