Skip to content

fix(tui): isolate embedded event listener failures#100117

Merged
steipete merged 2 commits into
openclaw:mainfrom
cxbAsDev:fix/embedded-backend-void-async-catch
Jul 5, 2026
Merged

fix(tui): isolate embedded event listener failures#100117
steipete merged 2 commits into
openclaw:mainfrom
cxbAsDev:fix/embedded-backend-void-async-catch

Conversation

@cxbAsDev

@cxbAsDev cxbAsDev commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

EmbeddedTuiBackend.handleAgentEvent was declared async even though it contains no asynchronous work. When a TUI event consumer threw synchronously, that unnecessary async boundary converted the error into a rejected promise; the fire-and-forget agent-event callback did not observe it, so Node could terminate the TUI for an unhandled rejection.

Why This Change Was Made

Keep the embedded event handler synchronous and call it directly from onAgentEvent. This restores the existing shared event-bus contract: notifyListeners isolates synchronous listener failures while continuing to notify other consumers. A focused regression injects a throwing TUI consumer through the real shared listener helper.

This supersedes the original noop .catch() suggestion. Removing the redundant async boundary fixes the root cause, preserves event ordering, and reduces production code instead of adding a silent catch path.

User Impact

Embedded/local TUI sessions no longer risk a process-level unhandled rejection when a TUI event consumer throws. Normal agent, chat, lifecycle, and local PTY behavior remains unchanged.

Evidence

Focused regression

$ node scripts/run-vitest.mjs src/tui/embedded-backend.test.ts
Test Files  1 passed (1)
Tests       43 passed (43)

The new regression starts a local run, routes an assistant event through notifyListeners, injects a throwing backend.onEvent consumer, and verifies the event bus contains the failure.

Local TUI PTY

$ OPENCLAW_TUI_PTY_INCLUDE_LOCAL=1 node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts
Test Files  2 passed (2)
Tests       17 passed (17)

This includes the slower tui --local smoke, with only the external model endpoint mocked.

Non-test runtime injection

Ran the real source backend and global agent-event bus outside Vitest with Node's strict unhandled-rejection mode. The harness installed an active embedded run, made backend.onEvent throw, emitted an assistant event through emitAgentEvent, waited one event-loop turn, and then stopped the backend.

$ node --unhandled-rejections=strict --import tsx --input-type=module -e '<runtime injection>'
TUI_EVENT_FAILURE_CONTAINED process_alive=true
EXIT_CODE=0

Without the synchronous repair, the thrown consumer error crosses the unnecessary async boundary as a rejected promise and strict Node exits nonzero. With this branch, the process remains alive because notifyListeners contains the synchronous listener failure.

Review

$ .agents/skills/autoreview/scripts/autoreview --mode local --stream-engine-output
autoreview clean: no accepted/actionable findings reported
overall: patch is correct (0.97)

Broad changed gate

Testbox-through-Crabbox provider blacksmith-testbox:

Scope

  • Production: src/tui/embedded-backend.ts (net -1 line)
  • Regression: src/tui/embedded-backend.test.ts
  • No config, protocol, dependency, storage, or public API changes.

@clawsweeper

clawsweeper Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 4, 2026, 10:35 PM ET / 02:35 UTC.

Summary
The branch makes embedded TUI agent-event handling synchronous and adds a regression for a throwing TUI event consumer routed through the shared listener helper.

PR surface: Source -1, Tests +41. Total +40 across 2 files.

Reproducibility: yes. Source inspection shows current main discards a promise returned by an async embedded event handler while the shared bus only isolates synchronous listener throws, and the PR body includes strict unhandled-rejection runtime proof for the repaired path.

Review metrics: none identified.

Root-cause cluster
Relationship: canonical
Canonical: #100117
Summary: This PR is the active canonical fix for the embedded TUI event-listener exception isolation bug; the earlier catch-based PR is closed unmerged and superseded.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Next step before merge

  • [P2] No repair lane is needed because the PR already contains the focused fix, regression coverage, and sufficient real behavior proof for maintainer review.

Security
Cleared: The current diff only changes embedded TUI event handling and a colocated test, with no dependency, workflow, credential, storage, protocol, or supply-chain surface changed.

Review details

Best possible solution:

Land the focused synchronous-handler repair with the regression once normal maintainer review and required checks are satisfied.

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

Yes. Source inspection shows current main discards a promise returned by an async embedded event handler while the shared bus only isolates synchronous listener throws, and the PR body includes strict unhandled-rejection runtime proof for the repaired path.

Is this the best way to solve the issue?

Yes. Removing the unnecessary async boundary is narrower and more maintainable than adding a local catch-and-swallow path because it reuses the existing shared listener isolation contract.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against eb2f9bece730.

Label changes

Label justifications:

  • P2: The PR fixes a narrow embedded TUI availability bug with limited blast radius and no evidence of a release-blocking outage.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes after-fix terminal proof for strict unhandled-rejection runtime injection plus focused TUI validation, and the current head has a passing real-behavior-proof check.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix terminal proof for strict unhandled-rejection runtime injection plus focused TUI validation, and the current head has a passing real-behavior-proof check.
Evidence reviewed

PR surface:

Source -1, Tests +41. Total +40 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 3 4 -1
Tests 1 41 0 +41
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 44 4 +40

What I checked:

  • Root policy read: Read the full root AGENTS.md and applied the OpenClaw PR-review requirements for whole-path review, scoped policy, proof assessment, and read-only cleanup. (AGENTS.md:1, eb2f9bece730)
  • Scoped TUI policy read: Read the TUI scoped guide; its local PTY and embedded-backend proof cautions shaped the real-behavior proof assessment. (src/tui/AGENTS.md:1, eb2f9bece730)
  • Current-main bug shape: Current main registers an agent-event listener that discards the promise from async handleAgentEvent, so a synchronous onEvent throw becomes a rejected promise outside notifyListeners' synchronous catch boundary. (src/tui/embedded-backend.ts:324, eb2f9bece730)
  • Shared listener contract: notifyListeners catches synchronous listener exceptions and continues iterating, and emitAgentEvent dispatches agent-event listeners through that helper. (src/shared/listeners.ts:2, eb2f9bece730)
  • PR head fix shape: The PR head removes the async boundary, keeps the registered listener synchronous, and leaves the handler body synchronous. (src/tui/embedded-backend.ts:324, 3494071a1382)
  • Regression coverage: The added regression imports notifyListeners, replays the registered agent listener through it, installs a throwing backend.onEvent, and asserts the dispatch does not throw. (src/tui/embedded-backend.test.ts:242, 3494071a1382)

Likely related people:

  • steipete: Recent merged TUI lifecycle work touched EmbeddedTuiBackend, and steipete authored the current no-content-drift repair commit on this PR. (role: recent adjacent contributor; confidence: high; commits: 1b8d83767445, 3494071a1382; files: src/tui/embedded-backend.ts, src/tui/embedded-backend.test.ts, src/tui/tui.ts)
  • NianJiuZst: Current checkout blame points the embedded event subscription and async handler lines to commit efe4a59...; the history is grafted/shallow around that commit, so this is a routing signal rather than full feature ownership. (role: current-line provenance; confidence: medium; commits: efe4a59ceb75; files: src/tui/embedded-backend.ts, src/infra/agent-events.ts, src/shared/listeners.ts)
  • vignesh07: CONTRIBUTING.md lists Vignesh for TUI ownership, making this a useful human routing candidate even though the focused git blame points elsewhere. (role: documented TUI area owner; confidence: medium; files: CONTRIBUTING.md, src/tui/embedded-backend.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (9 earlier review cycles; latest 8 shown)
  • reviewed 2026-07-05T00:24:12.909Z sha 8f9a14c :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-05T00:47:57.002Z sha 8f9a14c :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-05T00:52:46.781Z sha 8f9a14c :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-05T01:46:19.249Z sha 761e0dc :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-05T01:51:59.798Z sha 761e0dc :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-05T02:06:41.786Z sha 761e0dc :: needs maintainer review before merge. :: none
  • reviewed 2026-07-05T02:17:07.796Z sha 761e0dc :: needs maintainer review before merge. :: none
  • reviewed 2026-07-05T02:26:39.529Z sha 0a21c5a :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jul 5, 2026
@cxbAsDev

cxbAsDev commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Proof

Behavior

The onAgentEvent callback in EmbeddedTuiBackend now attaches a .catch(() => {}) to the fire-and-forget handleAgentEvent(evt) promise, preventing an unhandled rejection from crashing the TUI process.

Source

src/tui/embedded-backend.ts:325

this.unsubscribe = onAgentEvent((evt) => {
  void this.handleAgentEvent(evt).catch(() => {});
});

Test output

$ pnpm test -- src/tui/embedded-backend.test.ts --run
[test] starting test/vitest/vitest.tui.config.ts
 ✓ |tui| src/tui/embedded-backend.test.ts (41 tests) 3511ms

 Test Files  1 passed (1)
      Tests  41 passed (41)
   Duration  4.88s

[test] passed 1 Vitest shard in 14.29s

Notes

  • This is a defensive fix; the callback is fire-and-forget by design.
  • The .catch(() => {}) pattern is already used elsewhere for similar fire-and-forget handlers.
  • No new tests were added because the failure mode is an unhandled rejection in external event plumbing; existing tests continue to pass.

@cxbAsDev

cxbAsDev commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jul 5, 2026
@steipete
steipete force-pushed the fix/embedded-backend-void-async-catch branch from 8f9a14c to 761e0dc Compare July 5, 2026 01:41
@steipete steipete changed the title fix(tui): suppress unhandled rejection from async agent event handler fix(tui): isolate embedded event listener failures Jul 5, 2026
@steipete

steipete commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@harjothkhara

Copy link
Copy Markdown
Contributor

Non-maintainer proof pass on the latest head 761e0dc8e5a9:

  • The current shape looks materially stronger than the earlier noop-catch version. emitAgentEvent dispatches registered listeners through notifyListeners, and that helper only isolates synchronous listener throws. Keeping the onAgentEvent callback and handleAgentEvent synchronous lets a throwing backend.onEvent stay inside that existing bus instead of becoming an unobserved rejected promise.
  • The added regression hits the right seam: it routes the registered agent-event listener through notifyListeners([registeredListener], evt) and throws from backend.onEvent, so it covers the event-bus isolation contract rather than only calling the TUI handler directly.
  • The earlier ClawSweeper review asked for proof outside ordinary unit tests. The PR body now includes a strict unhandled-rejection runtime injection plus Testbox check:changed evidence, and the current check rollup shows the Real behavior proof jobs and CI green on this head.

I do not see a code-change blocker from this pass. The remaining maintainer decision is whether the posted runtime-injection proof is enough substitute for a fully interactive embedded TUI transcript; the scoped TUI guide still warns that PTY smoke does not prove every embedded backend runtime path.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 5, 2026
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: slack Channel integration: slack app: android App: android gateway Gateway runtime agents Agent runtime and tooling extensions: qa-lab size: L and removed size: XS labels Jul 5, 2026
@steipete
steipete force-pushed the fix/embedded-backend-void-async-catch branch from 0a21c5a to 3494071 Compare July 5, 2026 02:30
@openclaw-barnacle openclaw-barnacle Bot added size: XS and removed docs Improvements or additions to documentation channel: slack Channel integration: slack app: android App: android gateway Gateway runtime agents Agent runtime and tooling extensions: qa-lab size: L labels Jul 5, 2026
@steipete
steipete merged commit f400eb7 into openclaw:main Jul 5, 2026
107 of 111 checks passed
@steipete

steipete commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Merged via squash.

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

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants