* fix(gateway): pass actual version to Control UI client instead of "dev"
The GatewayClient, CLI WS client, and browser Control UI all sent
"dev" as their clientVersion during handshake, making it impossible
to distinguish builds in gateway logs and health snapshots.
- GatewayClient and CLI WS client now use the resolved VERSION constant
- Control UI reads serverVersion from the bootstrap endpoint and
forwards it when connecting
- Bootstrap contract extended with serverVersion field
Closes#35209
* Gateway: fix control-ui version version-reporting consistency
* Control UI: guard deferred bootstrap connect after disconnect
* fix(ui): accept same-origin http and relative gateway URLs for client version
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(gateway): correct launchctl command sequence for gateway restart (closes#20030)
* fix(restart): expand HOME and escape label in launchctl plist path
* fix(restart): poll port free after SIGKILL to prevent EADDRINUSE restart loop
When cleanStaleGatewayProcessesSync() kills a stale gateway process,
the kernel may not immediately release the TCP port. Previously the
function returned after a fixed 500ms sleep (300ms SIGTERM + 200ms
SIGKILL), allowing triggerOpenClawRestart() to hand off to systemd
before the port was actually free. The new systemd process then raced
the dying socket for port 18789, hit EADDRINUSE, and exited with
status 1, causing systemd to retry indefinitely — the zombie restart
loop reported in #33103.
Fix: add waitForPortFreeSync() that polls lsof at 50ms intervals for
up to 2 seconds after SIGKILL. cleanStaleGatewayProcessesSync() now
blocks until the port is confirmed free (or the budget expires with a
warning) before returning. The increased SIGTERM/SIGKILL wait budgets
(600ms / 400ms) also give slow processes more time to exit cleanly.
Fixes#33103
Related: #28134
* fix: add EADDRINUSE retry and TIME_WAIT port-bind checks for gateway startup
* fix(ports): treat EADDRNOTAVAIL as non-retryable and fix flaky test
* fix(gateway): hot-reload agents.defaults.models allowlist changes
The reload plan had a rule for `agents.defaults.model` (singular) but
not `agents.defaults.models` (plural — the allowlist array). Because
`agents.defaults.models` does not prefix-match `agents.defaults.model.`,
it fell through to the catch-all `agents` tail rule (kind=none), so
allowlist edits in openclaw.json were silently ignored at runtime.
Add a dedicated reload rule so changes to the models allowlist trigger
a heartbeat restart, which re-reads the config and serves the updated
list to clients.
Fixes#33600
Co-authored-by: HCL <chenglunhu@gmail.com>
Signed-off-by: HCL <chenglunhu@gmail.com>
* test(restart): 100% branch coverage — audit round 2
Audit findings fixed:
- remove dead guard: terminateStaleProcessesSync pids.length===0 check was
unreachable (only caller cleanStaleGatewayProcessesSync already guards)
- expose __testing.callSleepSyncRaw so sleepSync's real Atomics.wait path
can be unit-tested directly without going through the override
- fix broken sleepSync Atomics.wait test: previous test set override=null
but cleanStaleGatewayProcessesSync returned before calling sleepSync —
replaced with direct callSleepSyncRaw calls that actually exercise L36/L42-47
- fix pid collision: two tests used process.pid+304 (EPERM + dead-at-SIGTERM);
EPERM test changed to process.pid+305
- fix misindented tests: 'deduplicates pids' and 'lsof status 1 container
edge case' were outside their intended describe blocks; moved to correct
scopes (findGatewayPidsOnPortSync and pollPortOnce respectively)
- add missing branch tests:
- status 1 + non-empty stdout with zero openclaw pids → free:true (L145)
- mid-loop non-openclaw cmd in &&-chain (L67)
- consecutive p-lines without c-line between them (L67)
- invalid PID in p-line (p0 / pNaN) — ternary false branch (L67)
- unknown lsof output line (else-if false branch L69)
Coverage: 100% stmts / 100% branch / 100% funcs / 100% lines (36 tests)
* test(restart): fix stale-pid test typing for tsgo
* fix(gateway): address lifecycle review findings
* test(update): make restart-helper path assertions windows-safe
---------
Signed-off-by: HCL <chenglunhu@gmail.com>
Co-authored-by: Glucksberg <markuscontasul@gmail.com>
Co-authored-by: Efe Büken <efe@arven.digital>
Co-authored-by: Riccardo Marino <rmarino@apple.com>
Co-authored-by: HCL <chenglunhu@gmail.com>
Synthesize runtime state transition fixes for compaction tool-use integrity and long-running handler backpressure.
Sources: #33630, #33583
Co-authored-by: Kevin Shenghui <shenghuikevin@gmail.com>
Co-authored-by: Theo Tarr <theodore@tarr.com>
Complete the stop reason propagation chain so ACP clients can
distinguish end_turn from max_tokens:
- server-chat.ts: emitChatFinal accepts optional stopReason param,
includes it in the final payload, reads it from lifecycle event data
- translator.ts: read stopReason from the final payload instead of
hardcoding end_turn
Chain: LLM API → run.ts (meta.stopReason) → agent.ts (lifecycle event)
→ server-chat.ts (final payload) → ACP translator (PromptResponse)
* fix(gateway): flush throttled delta before emitChatFinal
The 150ms throttle in emitChatDelta can suppress the last text chunk
before emitChatFinal fires, causing streaming clients (e.g. ACP) to
receive truncated responses. The final event carries the complete text,
but clients that build responses incrementally from deltas miss the
tail end.
Flush one last unthrottled delta with the complete buffered text
immediately before sending the final event. This ensures all streaming
consumers have the full response without needing to reconcile deltas
against the final payload.
* fix(gateway): avoid duplicate delta flush when buffer unchanged
Track the text length at the time of the last broadcast. The flush in
emitChatFinal now only sends a delta if the buffer has grown since the
last broadcast, preventing duplicate sends when the final delta passed
the 150ms throttle and was already broadcast.
* fix(gateway): honor heartbeat suppression in final delta flush
* test(gateway): add final delta flush and dedupe coverage
* fix(gateway): skip final flush for silent lead fragments
* docs(changelog): note gateway final-delta flush fix credits
---------
Co-authored-by: Jonathan Taylor <visionik@pobox.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
## Overview
This PR enables external channel plugins (loaded via Plugin SDK) to access
advanced runtime features like AI response dispatching, which were previously
only available to built-in channels.
## Changes
### src/gateway/server-channels.ts
- Import PluginRuntime type
- Add optional channelRuntime parameter to ChannelManagerOptions
- Pass channelRuntime to channel startAccount calls via conditional spread
- Ensures backward compatibility (field is optional)
### src/gateway/server.impl.ts
- Import createPluginRuntime from plugins/runtime
- Create and pass channelRuntime to channel manager
### src/channels/plugins/types.adapters.ts
- Import PluginRuntime type
- Add comprehensive documentation for channelRuntime field
- Document available features, use cases, and examples
- Improve type safety (use imported PluginRuntime type vs inline import)
## Benefits
External channel plugins can now:
- Generate AI-powered responses using dispatchReplyWithBufferedBlockDispatcher
- Access routing, text processing, and session management utilities
- Use command authorization and group policy resolution
- Maintain feature parity with built-in channels
## Backward Compatibility
- channelRuntime field is optional in ChannelGatewayContext
- Conditional spread ensures it's only passed when explicitly provided
- Existing channels without channelRuntime support continue to work unchanged
- No breaking changes to channel plugin API
## Testing
- Email channel plugin successfully uses channelRuntime for AI responses
- All existing built-in channels (slack, discord, telegram, etc.) work unchanged
- Gateway loads and runs without errors when channelRuntime is provided
Take the safe, tested subset from #32367:\n- per-channel startup connect grace in health monitor\n- tool-context channel-provider fallback for message actions\n\nCo-authored-by: Munem Hashmi <munem.hashmi@gmail.com>
When controlUiBasePath is set, classifyControlUiRequest returned
method-not-allowed (405) for all non-GET/HEAD requests under basePath,
blocking plugin webhook handlers (BlueBubbles, Mattermost, etc.) from
receiving POST requests. This is a 2026.3.1 regression.
Return not-control-ui instead, matching the empty-basePath behavior, so
requests fall through to plugin HTTP handlers. Remove the now-dead
method-not-allowed type variant, handler branch, and utility function.
Closes#31983Closes#32275
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>