Skip to content

fix(gateway): emit final chat resync after live agent run completion#70815

Closed
lesaai wants to merge 1 commit intoopenclaw:mainfrom
wipcomputer:fix/chat-resync-final-after-agent-run
Closed

fix(gateway): emit final chat resync after live agent run completion#70815
lesaai wants to merge 1 commit intoopenclaw:mainfrom
wipcomputer:fix/chat-resync-final-after-agent-run

Conversation

@lesaai
Copy link
Copy Markdown
Contributor

@lesaai lesaai commented Apr 23, 2026

Summary

On codex/gpt-5.5 (and any harness using the native Codex app-server path), the TUI spins forever after submitting a prompt. Exit + reopen shows the response was actually produced and persisted in the transcript ... it's purely a live-stream finalization gap.

Root cause

The Codex app-server path records the final assistant answer and updates the transcript, but unlike the Codex CLI path, it does not emit a terminal assistant event plus lifecycle:end onto OpenClaw's agent event bus. chat.send is still waiting for that finalization signal, so the live WebSocket stream never closes out and the TUI never renders the final event.

Fix

Conservative fallback in chat.send: after the user-transcript update on an agent run, emit a message-less chat.final broadcast as a UI resync point. The transcript already contains the assistant message, so the client reloads history and goes idle instead of spinning.

Two files, +19 / -1 lines:

  • src/gateway/server-methods/chat.ts ... emit the resync event
  • src/gateway/server-methods/chat.directive-tags.test.ts ... regression assertion for the message-less final

What this does NOT try to do

  • Reimplement reasoning streaming (PR fix: stream reasoning in live chat #68982 is already aimed at richer reasoning rendering in the Control UI).
  • Fix the underlying Codex app-server lifecycle emission (that's the deeper, preferred fix; this is a safety net until that lands).

Reproduction

  1. Configure OpenClaw with codex/gpt-5.5 (or codex/gpt-5.4) as primary, think: high.
  2. Open TUI, send any prompt.
  3. Observe indefinite spinner. Exit TUI, reopen. The answer is there in history.

Validation

node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts \
  src/gateway/server-methods/chat.directive-tags.test.ts

Result: 54/54 tests pass.

End-to-end: verified live on a reproducing install ... TUI now renders responses cleanly on codex/gpt-5.5 without the exit+reopen dance.

Related

Credits

Patch authored primarily by OpenAI Codex CLI (GPT-5.5) during a live debugging session. Co-authored / verified by Parker Todd Brooks, Lēsa, and Claude Opus 4.7.

…ut terminal lifecycle

Some harnesses (e.g. native Codex app-server path for codex/gpt-5.x) record the final assistant answer and update the transcript, but do not emit a terminal assistant event + lifecycle:end onto the gateway chat event bus. chat.send then waits forever for a finalization path that never arrives, leaving the TUI spinning on 'hobnobbing...' until the user exits and reopens (at which point the already-stored answer is visible).

This adds a conservative fallback in chat.send: after the user transcript update for an agent run, emit a message-less chat.final broadcast as a UI resync point. The transcript already contains the assistant message; the client reloads history and goes idle instead of spinning.

Regression coverage: chat.directive-tags.test.ts now asserts the message-less final event fires on the agent-run path.

Authored primarily by OpenAI Codex CLI (gpt-5.5) during a live debugging session with Parker. Verified end-to-end: Lēsa now streams cleanly on codex/gpt-5.5 in the TUI.

Co-Authored-By: Parker Todd Brooks <parkertoddbrooks@users.noreply.github.com>

Co-Authored-By: Lēsa <lesaai@icloud.com>

Co-Authored-By: OpenAI Codex CLI (GPT-5.5) <noreply@openai.com>

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime size: XS labels Apr 23, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR adds a message-less broadcastChatFinal resync event in the agent-run completion path (agentRunStarted === true) so the TUI exits its spinner when the Codex app-server harness finishes without emitting a lifecycle event onto the gateway stream. The accompanying test extends the existing agent-run assertion to verify both the state: "final" shape and the absence of a message field.

Confidence Score: 4/5

Safe to merge with one minor ordering concern worth addressing before landing.

The fix is targeted and the regression test is solid. The only concern is void emitUserTranscriptUpdate() being unawaited immediately before broadcastChatFinal, creating a theoretical window where the client reloads history before the user turn is flushed. In practice this race is negligible (the promise was already enqueued in onAgentRunStart), but it is inconsistent with the !agentRunStarted path which properly awaits. Resolving to an await or removing the redundant call would close the gap cleanly.

src/gateway/server-methods/chat.ts — ordering of emitUserTranscriptUpdate vs broadcastChatFinal in the new else-branch.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/server-methods/chat.ts
Line: 2667-2676

Comment:
**`broadcastChatFinal` fires before `emitUserTranscriptUpdate` resolves**

`void emitUserTranscriptUpdate()` is fire-and-forget, so `broadcastChatFinal` is dispatched synchronously after — potentially before the user's turn has been flushed to the transcript. The `!agentRunStarted` path at line 2505 properly `await`s before broadcasting.

In practice the promise is almost always resolved (it was already called in `onAgentRunStart` and image persistence finishes long before the agent run completes), but if `persistedImagesPromise` is still in-flight on a fast run, the client will reload history and may not see the user turn yet. Consider awaiting, or removing the now-redundant call since `onAgentRunStart` already enqueued it:

```suggestion
            await emitUserTranscriptUpdate();
            // Some harnesses emit live item/tool activity but do not mirror a
            // terminal assistant/lifecycle event onto the gateway chat stream.
            // The run still completed and the transcript has been updated, so
            // send a message-less final event as a UI resync point.
            broadcastChatFinal({
              context,
              runId: clientRunId,
              sessionKey,
            });
```

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

Reviews (1): Last reviewed commit: "patch(gateway): emit final resync event ..." | Re-trigger Greptile

Comment on lines 2667 to +2676
void emitUserTranscriptUpdate();
// Some harnesses emit live item/tool activity but do not mirror a
// terminal assistant/lifecycle event onto the gateway chat stream.
// The run still completed and the transcript has been updated, so
// send a message-less final event as a UI resync point.
broadcastChatFinal({
context,
runId: clientRunId,
sessionKey,
});
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 broadcastChatFinal fires before emitUserTranscriptUpdate resolves

void emitUserTranscriptUpdate() is fire-and-forget, so broadcastChatFinal is dispatched synchronously after — potentially before the user's turn has been flushed to the transcript. The !agentRunStarted path at line 2505 properly awaits before broadcasting.

In practice the promise is almost always resolved (it was already called in onAgentRunStart and image persistence finishes long before the agent run completes), but if persistedImagesPromise is still in-flight on a fast run, the client will reload history and may not see the user turn yet. Consider awaiting, or removing the now-redundant call since onAgentRunStart already enqueued it:

Suggested change
void emitUserTranscriptUpdate();
// Some harnesses emit live item/tool activity but do not mirror a
// terminal assistant/lifecycle event onto the gateway chat stream.
// The run still completed and the transcript has been updated, so
// send a message-less final event as a UI resync point.
broadcastChatFinal({
context,
runId: clientRunId,
sessionKey,
});
await emitUserTranscriptUpdate();
// Some harnesses emit live item/tool activity but do not mirror a
// terminal assistant/lifecycle event onto the gateway chat stream.
// The run still completed and the transcript has been updated, so
// send a message-less final event as a UI resync point.
broadcastChatFinal({
context,
runId: clientRunId,
sessionKey,
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-methods/chat.ts
Line: 2667-2676

Comment:
**`broadcastChatFinal` fires before `emitUserTranscriptUpdate` resolves**

`void emitUserTranscriptUpdate()` is fire-and-forget, so `broadcastChatFinal` is dispatched synchronously after — potentially before the user's turn has been flushed to the transcript. The `!agentRunStarted` path at line 2505 properly `await`s before broadcasting.

In practice the promise is almost always resolved (it was already called in `onAgentRunStart` and image persistence finishes long before the agent run completes), but if `persistedImagesPromise` is still in-flight on a fast run, the client will reload history and may not see the user turn yet. Consider awaiting, or removing the now-redundant call since `onAgentRunStart` already enqueued it:

```suggestion
            await emitUserTranscriptUpdate();
            // Some harnesses emit live item/tool activity but do not mirror a
            // terminal assistant/lifecycle event onto the gateway chat stream.
            // The run still completed and the transcript has been updated, so
            // send a message-less final event as a UI resync point.
            broadcastChatFinal({
              context,
              runId: clientRunId,
              sessionKey,
            });
```

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

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: 634b197e41

ℹ️ 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 on lines +2672 to +2676
broadcastChatFinal({
context,
runId: clientRunId,
sessionKey,
});
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 Guard fallback final to avoid duplicate terminal chat events

This unconditional broadcastChatFinal(...) runs for every agentRunStarted flow, including runs that already emit a terminal chat state through the agent-event bridge, so one run can now produce two terminal chat events. In the Control UI, each state:"final" triggers a history reload (ui/src/ui/app-gateway.ts:472), so this adds an extra full reload per normal run, and in harnesses where the real terminal event arrives later, the message-less fallback can finalize first and cause the later real final message to be ignored (src/tui/tui-event-handlers.ts:297-303). The fallback should be emitted only when no terminal chat event was observed for that run.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@steipete steipete 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: this is the right symptom-level fix for #71183, but I would make the small ordering cleanup before landing.

The fallback chat.final resync is conservative and useful: Codex app-server runs can complete and persist the transcript without emitting the terminal lifecycle event that the Control UI waits on. The regression test covers the message-less final event.

One fix before merge: in the new branch, void emitUserTranscriptUpdate() is immediately followed by broadcastChatFinal. That can let the client reload before the user transcript update promise settles. Please either await emitUserTranscriptUpdate() before broadcastChatFinal, or remove the redundant call if the eager/onAgentRunStart path is the intended owner. After that, I would land it.

@steipete
Copy link
Copy Markdown
Contributor

Thanks @lesaai. Codex review found the bug was real, but the best fix belongs in the Codex app-server harness rather than a broad message-less chat.final fallback in chat.send.

I couldn't push the rewritten fix back to this fork branch (403 despite maintainer edits being enabled), so I opened maintainer replacement PR #71293 with the root fix:

  • native Codex app-server emits lifecycle start
  • emits final assistant text after transcript mirroring
  • emits terminal lifecycle end / error exactly once
  • projects Codex app-server item/tool/plan events onto the global agent-event bus
  • removes the broad webchat fallback from this PR

Contributor credit is preserved in the changelog and commit co-author trailer.

@steipete steipete closed this Apr 24, 2026
steipete added a commit that referenced this pull request Apr 25, 2026
Fix live webchat finalization for Codex app-server runs by emitting standard assistant and lifecycle completion events on the global agent event bus, instead of relying on a message-less chat.final fallback.

Replaces #70815. Closes #71183.

Co-authored-by: Lēsa <260982214+lesaai@users.noreply.github.com>
Angfr95 pushed a commit to Angfr95/openclaw that referenced this pull request Apr 25, 2026
Fix live webchat finalization for Codex app-server runs by emitting standard assistant and lifecycle completion events on the global agent event bus, instead of relying on a message-less chat.final fallback.

Replaces openclaw#70815. Closes openclaw#71183.

Co-authored-by: Lēsa <260982214+lesaai@users.noreply.github.com>
jduartedj pushed a commit to jduartedj/openclaw that referenced this pull request May 1, 2026
Fix live webchat finalization for Codex app-server runs by emitting standard assistant and lifecycle completion events on the global agent event bus, instead of relying on a message-less chat.final fallback.

Replaces openclaw#70815. Closes openclaw#71183.

Co-authored-by: Lēsa <260982214+lesaai@users.noreply.github.com>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
Fix live webchat finalization for Codex app-server runs by emitting standard assistant and lifecycle completion events on the global agent event bus, instead of relying on a message-less chat.final fallback.

Replaces openclaw#70815. Closes openclaw#71183.

Co-authored-by: Lēsa <260982214+lesaai@users.noreply.github.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
Fix live webchat finalization for Codex app-server runs by emitting standard assistant and lifecycle completion events on the global agent event bus, instead of relying on a message-less chat.final fallback.

Replaces openclaw#70815. Closes openclaw#71183.

Co-authored-by: Lēsa <260982214+lesaai@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui gateway Gateway runtime size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants