mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 07:42:44 +00:00
fix(typing): stop keepalive restarts after run completion (land #27413, thanks @widingmarcus-cyber)
Co-authored-by: Marcus Widing <widing.marcus@gmail.com>
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram/Inline buttons: allow callback-query button handling in groups (including `/models` follow-up buttons) when group policy authorizes the sender, by removing the redundant callback allowlist gate that blocked open-policy groups. (#27343) Thanks @GodsBoy.
|
- Telegram/Inline buttons: allow callback-query button handling in groups (including `/models` follow-up buttons) when group policy authorizes the sender, by removing the redundant callback allowlist gate that blocked open-policy groups. (#27343) Thanks @GodsBoy.
|
||||||
- Telegram/Streaming preview: when finalizing without an existing preview message, prime pending preview text with final answer before stop-flush so users do not briefly see stale 1-2 word fragments (for example `no` before `no problem`). (#27449) Thanks @emanuelst for the original fix direction in #19673.
|
- Telegram/Streaming preview: when finalizing without an existing preview message, prime pending preview text with final answer before stop-flush so users do not briefly see stale 1-2 word fragments (for example `no` before `no problem`). (#27449) Thanks @emanuelst for the original fix direction in #19673.
|
||||||
- Typing/Main reply pipeline: always mark dispatch idle in `agent-runner` finalization so typing cleanup runs even when dispatcher `onIdle` does not fire, preventing stuck typing indicators after run completion. (#27250) Thanks @Sid-Qin.
|
- Typing/Main reply pipeline: always mark dispatch idle in `agent-runner` finalization so typing cleanup runs even when dispatcher `onIdle` does not fire, preventing stuck typing indicators after run completion. (#27250) Thanks @Sid-Qin.
|
||||||
|
- Typing/Run completion race: prevent post-run keepalive ticks from re-triggering typing callbacks by guarding `triggerTyping()` with `runComplete`, with regression coverage for no-restart behavior during run-complete/dispatch-idle boundaries. (#27413) Thanks @widingmarcus-cyber.
|
||||||
- Daemon/macOS launchd: forward proxy env vars into supervised service environments, keep LaunchAgent `KeepAlive=true` semantics, and harden restart sequencing to `print -> bootout -> wait old pid exit -> bootstrap -> kickstart`. (#27276) thanks @frankekn.
|
- Daemon/macOS launchd: forward proxy env vars into supervised service environments, keep LaunchAgent `KeepAlive=true` semantics, and harden restart sequencing to `print -> bootout -> wait old pid exit -> bootstrap -> kickstart`. (#27276) thanks @frankekn.
|
||||||
- Web tools/Proxy: route `web_search` provider HTTP calls (Brave, Perplexity, xAI, Gemini, Kimi), redirect resolution, and `web_fetch` through a shared proxy-aware SSRF guard path so gateway installs behind `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY` no longer fail with transport `fetch failed` errors. (#27430) thanks @kevinWangSheng.
|
- Web tools/Proxy: route `web_search` provider HTTP calls (Brave, Perplexity, xAI, Gemini, Kimi), redirect resolution, and `web_fetch` through a shared proxy-aware SSRF guard path so gateway installs behind `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY` no longer fail with transport `fetch failed` errors. (#27430) thanks @kevinWangSheng.
|
||||||
- Android/Node invoke: remove native gateway WebSocket `Origin` header to avoid false origin rejections, unify invoke command registry/policy/error parsing paths, and keep command availability checks centralized to reduce dispatcher/advertisement drift. (#27257) Thanks @obviyus.
|
- Android/Node invoke: remove native gateway WebSocket `Origin` header to avoid false origin rejections, unify invoke command registry/policy/error parsing paths, and keep command availability checks centralized to reduce dispatcher/advertisement drift. (#27257) Thanks @obviyus.
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ describe("typing controller", () => {
|
|||||||
typing.markDispatchIdle();
|
typing.markDispatchIdle();
|
||||||
}
|
}
|
||||||
await vi.advanceTimersByTimeAsync(2_000);
|
await vi.advanceTimersByTimeAsync(2_000);
|
||||||
expect(onReplyStart, testCase.name).toHaveBeenCalledTimes(5);
|
expect(onReplyStart, testCase.name).toHaveBeenCalledTimes(testCase.first === "run" ? 3 : 5);
|
||||||
|
|
||||||
if (testCase.second === "run") {
|
if (testCase.second === "run") {
|
||||||
typing.markRunComplete();
|
typing.markRunComplete();
|
||||||
@@ -150,7 +150,7 @@ describe("typing controller", () => {
|
|||||||
typing.markDispatchIdle();
|
typing.markDispatchIdle();
|
||||||
}
|
}
|
||||||
await vi.advanceTimersByTimeAsync(2_000);
|
await vi.advanceTimersByTimeAsync(2_000);
|
||||||
expect(onReplyStart, testCase.name).toHaveBeenCalledTimes(5);
|
expect(onReplyStart, testCase.name).toHaveBeenCalledTimes(testCase.first === "run" ? 3 : 5);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
83
src/auto-reply/reply/typing-persistence.test.ts
Normal file
83
src/auto-reply/reply/typing-persistence.test.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest";
|
||||||
|
import { createTypingController } from "./typing.js";
|
||||||
|
|
||||||
|
describe("typing persistence bug fix", () => {
|
||||||
|
let onReplyStartSpy: Mock;
|
||||||
|
let onCleanupSpy: Mock;
|
||||||
|
let controller: ReturnType<typeof createTypingController>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
onReplyStartSpy = vi.fn();
|
||||||
|
onCleanupSpy = vi.fn();
|
||||||
|
|
||||||
|
controller = createTypingController({
|
||||||
|
onReplyStart: onReplyStartSpy,
|
||||||
|
onCleanup: onCleanupSpy,
|
||||||
|
typingIntervalSeconds: 6,
|
||||||
|
log: vi.fn(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT restart typing after markRunComplete is called", async () => {
|
||||||
|
// Start typing normally
|
||||||
|
await controller.startTypingLoop();
|
||||||
|
expect(onReplyStartSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Mark run as complete (but not yet dispatch idle)
|
||||||
|
controller.markRunComplete();
|
||||||
|
|
||||||
|
// Advance time to trigger the typing interval (6 seconds)
|
||||||
|
vi.advanceTimersByTime(6000);
|
||||||
|
|
||||||
|
// BUG: The typing loop should NOT call onReplyStart again
|
||||||
|
// because the run is already complete
|
||||||
|
expect(onReplyStartSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onReplyStartSpy).not.toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should stop typing when both runComplete and dispatchIdle are true", async () => {
|
||||||
|
// Start typing
|
||||||
|
await controller.startTypingLoop();
|
||||||
|
expect(onReplyStartSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Mark run complete
|
||||||
|
controller.markRunComplete();
|
||||||
|
expect(onCleanupSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Mark dispatch idle - should trigger cleanup
|
||||||
|
controller.markDispatchIdle();
|
||||||
|
expect(onCleanupSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// After cleanup, typing interval should not restart typing
|
||||||
|
vi.advanceTimersByTime(6000);
|
||||||
|
expect(onReplyStartSpy).toHaveBeenCalledTimes(1); // Still only the initial call
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent typing restart even if cleanup is delayed", async () => {
|
||||||
|
// Start typing
|
||||||
|
await controller.startTypingLoop();
|
||||||
|
expect(onReplyStartSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Mark run complete (but dispatch not idle yet - simulating cleanup delay)
|
||||||
|
controller.markRunComplete();
|
||||||
|
|
||||||
|
// Multiple typing intervals should NOT restart typing
|
||||||
|
vi.advanceTimersByTime(6000); // First interval
|
||||||
|
expect(onReplyStartSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(6000); // Second interval
|
||||||
|
expect(onReplyStartSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(6000); // Third interval
|
||||||
|
expect(onReplyStartSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Eventually dispatch becomes idle and triggers cleanup
|
||||||
|
controller.markDispatchIdle();
|
||||||
|
expect(onCleanupSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -99,6 +99,10 @@ export function createTypingController(params: {
|
|||||||
if (sealed) {
|
if (sealed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Late callbacks after a run completed should never restart typing.
|
||||||
|
if (runComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await onReplyStart?.();
|
await onReplyStart?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user