Skip to content

Fix/disable refresh during active turn#66395

Closed
Angfr95 wants to merge 1 commit intoopenclaw:mainfrom
Angfr95:fix/disable-refresh-during-active-turn
Closed

Fix/disable refresh during active turn#66395
Angfr95 wants to merge 1 commit intoopenclaw:mainfrom
Angfr95:fix/disable-refresh-during-active-turn

Conversation

@Angfr95
Copy link
Copy Markdown
Contributor

@Angfr95 Angfr95 commented Apr 14, 2026

Summary

  • Problem: The refresh chat data button was only disabled when chatLoading or disconnected, remaining clickable during active agent turns.
  • Why it matters: Clicking refresh mid-turn stalls the live run, resets chat state, and can wedge the gateway session.
  • What changed: Added chatSending, chatRunId, and chatStream guards to the refresh button's disabled condition.
  • What did NOT change (scope boundary): No changes to refresh logic, gateway behavior, or other controls. Only the disabled guard on the refresh button.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: The refresh button's disabled binding was missing the busy-state guards (chatSending, chatRunId, chatStream) that are already used by the model select and thinking select controls in the same file.
  • Missing detection / guardrail: No guard for the sending, running, or streaming phases of an agent turn on the refresh button specifically.
  • Contributing context (if known): The model select (line ~490) and thinking select (line ~560) both use a busy variable combining these four flags. The refresh button only had two of them.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: ui/src/ui/app-render.helpers.ts
  • Scenario the test should lock in: Refresh button disabled state includes chatSending, chatRunId, and chatStream
  • Why this is the smallest reliable guardrail: The disabled binding is declarative (Lit ?disabled), so it's evaluated on every render cycle.
  • Existing test that already covers this (if any): N/A
  • If no new test is added, why not: The fix aligns a Lit template binding with the existing busy-state pattern. The guards are reactive properties that Lit re-evaluates on each render. A unit test for a template binding in Lit requires a full render harness and would test the framework, not the logic.

User-visible / Behavior Changes

Refresh button is now grayed out and non-clickable while the agent is sending, running, or streaming a response.

Diagram (if applicable)

Before:
[send message] -> [chatSending=true, chatRunId set, chatStream active] -> refresh button still enabled -> click -> stall/reset

After:
[send message] -> [chatSending=true, chatRunId set, chatStream active] -> refresh button disabled -> no click possible

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Windows 11 x64
  • Runtime/container: Node v22.19.0, npm global install
  • Model/provider: OpenRouter (openrouter/auto)
  • Integration/channel (if any): Control UI WebChat
  • Relevant config (redacted): Default config with OpenRouter provider

Steps

  1. Open Control UI chat tab
  2. Send a message that triggers an agent response
  3. While the agent turn is active (sending/streaming), observe the refresh button

Expected

Refresh button should be disabled (grayed out) during the entire agent turn lifecycle.

Actual (before fix)

Refresh button remained clickable. Clicking it mid-turn reset the chat and stalled the run.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Before (refresh button clickable during active turn)

before

After (refresh button disabled during active turn)

after

Human Verification (required)

  • Verified scenarios: Sent messages via Control UI WebChat, confirmed button is disabled during agent turn, confirmed button re-enables after turn completes.
  • Edge cases checked: Button state on idle chat (enabled), button state during stream (disabled), button state after completion (re-enabled).
  • What you did not verify: Behavior with WebSocket disconnection mid-turn, behavior on mobile Control UI layout.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: Edge case where chatRunId or chatStream are set but the turn is actually stale, leaving the button permanently disabled.
    • Mitigation: The same guards are already used by model select and thinking select without reported issues. The existing lifecycle cleanup (resetChatStateForSessionSwitch) clears all three flags.

@openclaw-barnacle openclaw-barnacle Bot added channel: slack Channel integration: slack app: web-ui App: web-ui size: S labels Apr 14, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 14, 2026

Greptile Summary

This PR contains two separate fixes: (1) the advertised UI fix that adds chatSending, chatRunId, and chatStream guards to the refresh button's ?disabled binding, aligning it with the existing busy pattern on model-select and thinking-select controls; and (2) undescribed Slack socket-mode changes that convert three throw exits to runtime.error?.() + return, preventing unhandled rejections from crashing the gateway when auth fails permanently or reconnect attempts are exhausted.

The UI fix is correct and minimal. The Slack changes are sound (the finally block still handles cleanup), but they aren't mentioned in the PR title, description, or scope checklist — reviewers should be aware they are also reviewing a behavioral contract change in the Slack channel provider.

Confidence Score: 5/5

  • Safe to merge; both fixes are correct and the only findings are P2 improvements.
  • The UI disabled-state fix is a one-liner that matches the existing pattern exactly. The Slack provider changes convert throws to graceful returns — the finally block ensures cleanup runs either way and the intent is sound. The sole comment is a P2 test-coverage suggestion for the two untested max-reconnect paths.
  • The Slack provider changes (extensions/slack/src/monitor/provider.ts) are not described anywhere in the PR — reviewers should be aware they are reviewing those changes as well.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/slack/src/monitor/provider.gateway-crash.test.ts
Line: 33-44

Comment:
**Test coverage for max-reconnect paths**

The new test validates only the non-recoverable auth error branch (`isNonRecoverableSlackAuthError``return`). The two max-reconnect-attempts branches converted from `throw` to `return` in `provider.ts` — one in the `app.start()` catch block and one in the post-disconnect block — have no test coverage. If the `runtime.error?.()` call were removed or the `return` were accidentally reverted to `throw` in either of those paths, no test would catch it.

Consider adding two more cases to this describe block:

```ts
it("resolves instead of rejecting after exhausting reconnect attempts on start", async () => {
  // mock start() to throw a non-auth error N times (N = maxAttempts)
  ...
  await expect(monitorSlackProvider({ ... })).resolves.toBeUndefined();
});
```

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

Reviews (1): Last reviewed commit: "fix(ui): disable chat refresh button dur..." | Re-trigger Greptile

Comment on lines +33 to +44

describe("monitorSlackProvider - gateway crash prevention", () => {
it("resolves instead of rejecting on non-recoverable auth error", async () => {
await expect(
monitorSlackProvider({
botToken: "xoxb-test",
appToken: "xapp-test",
accountId: "default",
}),
).resolves.toBeUndefined();
});
});
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 Test coverage for max-reconnect paths

The new test validates only the non-recoverable auth error branch (isNonRecoverableSlackAuthErrorreturn). The two max-reconnect-attempts branches converted from throw to return in provider.ts — one in the app.start() catch block and one in the post-disconnect block — have no test coverage. If the runtime.error?.() call were removed or the return were accidentally reverted to throw in either of those paths, no test would catch it.

Consider adding two more cases to this describe block:

it("resolves instead of rejecting after exhausting reconnect attempts on start", async () => {
  // mock start() to throw a non-auth error N times (N = maxAttempts)
  ...
  await expect(monitorSlackProvider({ ... })).resolves.toBeUndefined();
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/slack/src/monitor/provider.gateway-crash.test.ts
Line: 33-44

Comment:
**Test coverage for max-reconnect paths**

The new test validates only the non-recoverable auth error branch (`isNonRecoverableSlackAuthError``return`). The two max-reconnect-attempts branches converted from `throw` to `return` in `provider.ts` — one in the `app.start()` catch block and one in the post-disconnect block — have no test coverage. If the `runtime.error?.()` call were removed or the `return` were accidentally reverted to `throw` in either of those paths, no test would catch it.

Consider adding two more cases to this describe block:

```ts
it("resolves instead of rejecting after exhausting reconnect attempts on start", async () => {
  // mock start() to throw a non-auth error N times (N = maxAttempts)
  ...
  await expect(monitorSlackProvider({ ... })).resolves.toBeUndefined();
});
```

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: 07c1dde3b8

ℹ️ 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".

@@ -593,14 +593,17 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
runtime.error?.(
`slack socket mode failed to start due to non-recoverable auth error — skipping channel (${formatUnknownError(err)})`,
);
throw err;
return;
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 Preserve fatal startup errors by rejecting monitor task

Returning here makes monitorSlackProvider resolve on non-recoverable startup failures, but src/gateway/server-channels.ts only records lastError when startAccount rejects (the catch path around lines 432-435). In the auth-failure case this now leaves runtime status with lastError: null, so Slack appears stopped without an actionable reason in channel status/UI even though startup failed due to invalid credentials.

Useful? React with 👍 / 👎.

@Angfr95 Angfr95 force-pushed the fix/disable-refresh-during-active-turn branch from 07c1dde to 28b4306 Compare April 24, 2026 04:16
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: 28b43061af

ℹ️ 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".

error = vi.fn();
}
class HTTPReceiver {requestListener = vi.fn();}
return { default: App, App, HTTPReceiver };
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 Provide SocketModeReceiver in mocked @slack/bolt export

This mock returns only App and HTTPReceiver, but monitorSlackProvider runs in socket mode (config.mode: "socket") and resolveSlackBoltInterop requires SocketModeReceiver as well. The test will fail early with Unable to resolve @slack/bolt App/HTTPReceiver exports before it reaches the auth-error path, so it neither validates the intended regression nor remains stable in CI.

Useful? React with 👍 / 👎.

Comment on lines +504 to +507
runtime.error?.(
`slack socket mode failed to start after ${reconnectAttempts} attempts — stopping channel (${formatUnknownError(err)})`,
);
return;
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 Propagate max-attempt startup failures to channel runtime

Returning when socket startup retries are exhausted treats a hard startup failure as a successful completion. In src/gateway/server-channels.ts, the channel runtime only records lastError in the startAccount rejection path, so this branch now stops/restarts without surfacing the failure reason to status/UI. This should reject (or explicitly set an error status) so operators can diagnose persistent startup failures.

Useful? React with 👍 / 👎.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: the latest contributor branch is now the intended UI-only guard and appears functionally correct, but an open maintainer PR, #74690, explicitly supersedes this UI-only portion, adds browser coverage for the disabled-state matrix, and is the canonical path for closing the underlying issue.

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Review details

Best possible solution:

Use #74690 as the canonical implementation path because it contains the same narrow Control UI guard plus focused browser coverage, then close #65522 when that maintainer PR lands and ships.

Security review:

Security review cleared: The latest diff only changes a Control UI button disabled binding and does not touch dependencies, workflows, permissions, secrets, network calls, or code execution paths.

What I checked:

Likely related people:

  • BunsDev: Requested changes on the mixed-scope revision, was pinged after the branch was narrowed, and opened the maintainer-labeled fix(control-ui): disable refresh during active runs #74690 that explicitly supersedes this UI-only PR with the same guard plus browser coverage. (role: maintainer reviewer and canonical follow-up owner; confidence: high; commits: a2c237228d79; files: ui/src/ui/app-render.helpers.ts, ui/src/ui/app-render.helpers.browser.test.ts)
  • Shakker: Available local blame/log history attributes the current renderChatControls, adjacent session controls, and chat lifecycle file state to d3c6a8f0fb6d09904edd0848d6d88afba576db2a; this is useful routing context even though the superseding review path is owned by BunsDev. (role: recent current-main maintainer for the touched UI files; confidence: medium; commits: d3c6a8f0fb6d; files: ui/src/ui/app-render.helpers.ts, ui/src/ui/chat/session-controls.ts, ui/src/ui/controllers/chat.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 1fb096f0e607.

Copy link
Copy Markdown
Member

@BunsDev BunsDev left a comment

Choose a reason for hiding this comment

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

Thanks for the Control UI fix. I am requesting changes because this PR is mixed-scope: the UI disabled-state guard is valid, but the Slack provider lifecycle edits introduce a blocking regression by treating permanent startup failures as clean resolution, which can hide failure status and allow restart loops. Please split this into a UI-only PR for #65522, or remove the Slack changes and add focused Control UI coverage. I am superseding the UI fix with a clean local maintainer patch.

@Angfr95 Angfr95 force-pushed the fix/disable-refresh-during-active-turn branch from 9f537a9 to 4cb375e Compare April 29, 2026 21:10
@openclaw-barnacle openclaw-barnacle Bot added size: XS and removed channel: slack Channel integration: slack size: S labels Apr 29, 2026
@Angfr95
Copy link
Copy Markdown
Contributor Author

Angfr95 commented Apr 29, 2026

Thanks for the Control UI fix. I am requesting changes because this PR is mixed-scope: the UI disabled-state guard is valid, but the Slack provider lifecycle edits introduce a blocking regression by treating permanent startup failures as clean resolution, which can hide failure status and allow restart loops. Please split this into a UI-only PR for #65522, or remove the Slack changes and add focused Control UI coverage. I am superseding the UI fix with a clean local maintainer patch.

@BunsDev Slack changes removed — PR is now UI-only, single commit touching only ui/src/ui/app-render.helpers.ts. Ready for re-review.

The refresh button was only guarded by chatLoading and connected state,
allowing clicks during active agent turns. This could stall the run,
reset chat state, and wedge the gateway.

Add chatSending, chatRunId, and chatStream guards to match the existing
busy-state pattern used by the model and thinking selects.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Control UI: refreshing chat data during an active turn can stall the run and wedge the page/gateway

2 participants