From ceace835563db76e661092292966be88b6b341cb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 02:18:49 -0700 Subject: [PATCH 1/3] fix(telegram): keep polling watchdog active for wedged runner --- CHANGELOG.md | 1 + .../telegram/src/polling-liveness.test.ts | 4 -- extensions/telegram/src/polling-liveness.ts | 9 +--- .../telegram/src/polling-session.test.ts | 54 +++++++++++++++++++ extensions/telegram/src/polling-session.ts | 1 - 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ca541b09d..395812e7470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing `every` values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys. - Telegram: remove the startup persisted-offset `getUpdates` preflight so polling restarts do not self-conflict before the runner starts. Fixes #69304. (#69779) Thanks @chinar-amrutkar. +- Telegram: keep the polling stall watchdog active even when grammY reports the runner as not running while its task is still pending, so a rebuilt transport cannot leave `getUpdates` silent until a manual gateway restart. Fixes #69064. Thanks @LDLoeb. - Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai. - Browser/downloads: seed managed Chrome profiles with OpenClaw download prefs and capture unmanaged click-triggered downloads under the guarded downloads directory, while explicit download waiters still own their target file. (#64558) Thanks @Pearcekieser. - Browser/Chrome: stop passing redundant `--disable-setuid-sandbox` when `browser.noSandbox` is enabled; `--no-sandbox` remains the effective sandbox opt-out. (#67939) Thanks @sebykrueger. diff --git a/extensions/telegram/src/polling-liveness.test.ts b/extensions/telegram/src/polling-liveness.test.ts index f41413838a2..b38b1bb886d 100644 --- a/extensions/telegram/src/polling-liveness.test.ts +++ b/extensions/telegram/src/polling-liveness.test.ts @@ -31,7 +31,6 @@ describe("TelegramPollingLivenessTracker", () => { expect( tracker.detectStall({ thresholdMs: POLL_STALL_THRESHOLD_MS, - runnerIsRunning: true, }), ).toBeNull(); @@ -45,7 +44,6 @@ describe("TelegramPollingLivenessTracker", () => { now = 120_001; const stall = tracker.detectStall({ thresholdMs: POLL_STALL_THRESHOLD_MS, - runnerIsRunning: true, }); expect(stall?.message).toContain("Polling stall detected (no completed getUpdates"); expect(stall?.message).toContain("inFlight=0 outcome=not-started"); @@ -54,7 +52,6 @@ describe("TelegramPollingLivenessTracker", () => { expect( tracker.detectStall({ thresholdMs: POLL_STALL_THRESHOLD_MS, - runnerIsRunning: true, }), ).toBeNull(); }); @@ -69,7 +66,6 @@ describe("TelegramPollingLivenessTracker", () => { now = 120_001; const stall = tracker.detectStall({ thresholdMs: POLL_STALL_THRESHOLD_MS, - runnerIsRunning: true, }); expect(stall?.message).toContain("active getUpdates stuck"); diff --git a/extensions/telegram/src/polling-liveness.ts b/extensions/telegram/src/polling-liveness.ts index 6fcd200f6fb..b57237de020 100644 --- a/extensions/telegram/src/polling-liveness.ts +++ b/extensions/telegram/src/polling-liveness.ts @@ -89,14 +89,7 @@ export class TelegramPollingLivenessTracker { this.#inFlightGetUpdates = Math.max(0, this.#inFlightGetUpdates - 1); } - detectStall(params: { - thresholdMs: number; - runnerIsRunning: boolean; - now?: number; - }): TelegramPollingStall | null { - if (!params.runnerIsRunning) { - return null; - } + detectStall(params: { thresholdMs: number; now?: number }): TelegramPollingStall | null { const now = params.now ?? this.#now(); const activeElapsed = this.#inFlightGetUpdates > 0 && this.#lastGetUpdatesStartedAt != null diff --git a/extensions/telegram/src/polling-session.test.ts b/extensions/telegram/src/polling-session.test.ts index 34cbc97d8ce..a29a03a6877 100644 --- a/extensions/telegram/src/polling-session.test.ts +++ b/extensions/telegram/src/polling-session.test.ts @@ -387,6 +387,60 @@ describe("TelegramPollingSession", () => { } }); + it("forces a restart when the runner task is pending but reports not running", async () => { + const abort = new AbortController(); + const firstRunnerStop = vi.fn(async () => undefined); + const secondRunnerStop = vi.fn(async () => undefined); + createTelegramBotMock.mockReturnValue(makeBot()); + + let firstTaskResolve: (() => void) | undefined; + const firstTask = new Promise((resolve) => { + firstTaskResolve = resolve; + }); + let cycle = 0; + runMock.mockImplementation(() => { + cycle += 1; + if (cycle === 1) { + return { + task: () => firstTask, + stop: async () => { + await firstRunnerStop(); + firstTaskResolve?.(); + }, + isRunning: () => false, + }; + } + return { + task: async () => { + abort.abort(); + }, + stop: secondRunnerStop, + isRunning: () => false, + }; + }); + + const watchdogHarness = installPollingStallWatchdogHarness(); + + const log = vi.fn(); + const session = createPollingSession({ + abortSignal: abort.signal, + log, + }); + + try { + const runPromise = session.runUntilAbort(); + const watchdog = await watchdogHarness.waitForWatchdog(); + watchdog?.(); + await runPromise; + + expect(runMock).toHaveBeenCalledTimes(2); + expect(firstRunnerStop).toHaveBeenCalledTimes(1); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Polling stall detected")); + } finally { + watchdogHarness.restore(); + } + }); + it("honors a custom polling stall threshold", async () => { const abort = new AbortController(); const botStop = vi.fn(async () => undefined); diff --git a/extensions/telegram/src/polling-session.ts b/extensions/telegram/src/polling-session.ts index 011b77de5db..11cc14f755a 100644 --- a/extensions/telegram/src/polling-session.ts +++ b/extensions/telegram/src/polling-session.ts @@ -295,7 +295,6 @@ export class TelegramPollingSession { const stall = liveness.detectStall({ thresholdMs: this.#stallThresholdMs, - runnerIsRunning: runner.isRunning(), }); if (stall) { this.#transportState.markDirty(); From 388270ffce55c6e9828a30f1d80c3198143f5023 Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Sat, 25 Apr 2026 04:19:56 -0500 Subject: [PATCH 2/3] fix(control-ui): clarify chat context details Summary: - Show full date and time in Control UI chat message footers. - Collapse assistant model/token/context metadata behind an explicit Context disclosure. - Update changelog attribution guidance to allow multi-author credited entries. Validation: - OPENCLAW_LOCAL_CHECK=0 pnpm test ui/src/ui/chat/grouped-render.test.ts - OPENCLAW_LOCAL_CHECK=0 pnpm test src/commands/gateway-status/helpers.test.ts - OPENCLAW_LOCAL_CHECK=0 pnpm check:changed - GitHub CI passed on f071a381778349d7ebe1404220549d3c2dc6a138 --- .../context-collapsed-full-date.png | Bin 0 -> 38846 bytes AGENTS.md | 2 +- CHANGELOG.md | 1 + src/commands/gateway-status/helpers.test.ts | 2 + ui/src/styles/chat/grouped.css | 76 +++++++++++++++++- ui/src/ui/chat/grouped-render.test.ts | 34 ++++++++ ui/src/ui/chat/grouped-render.ts | 70 +++++++++++++--- 7 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 .github/pr-assets/control-ui-chat-detail/context-collapsed-full-date.png diff --git a/.github/pr-assets/control-ui-chat-detail/context-collapsed-full-date.png b/.github/pr-assets/control-ui-chat-detail/context-collapsed-full-date.png new file mode 100644 index 0000000000000000000000000000000000000000..333ab7c2ce9661afafee69926cafcbbaeae59bf6 GIT binary patch literal 38846 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 Jvd$@?2>^u)R2~2T literal 0 HcmV?d00001 diff --git a/AGENTS.md b/AGENTS.md index cd94cb0417b..735147af333 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,7 +120,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work. - Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`. - Changelog user-facing only; pure test/internal usually no entry. -- Changelog placement: active version `### Changes`/`### Fixes`; at most one contributor mention, prefer `Thanks @user`. +- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). ## Git diff --git a/CHANGELOG.md b/CHANGELOG.md index 395812e7470..4aea049566e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing `every` values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys. +- Control UI/chat: collapse assistant token/model context details behind an explicit Context disclosure and show full dates in message footers, making historical transcript timing clear without noisy default metadata. (#71337) Thanks @BunsDev. - Telegram: remove the startup persisted-offset `getUpdates` preflight so polling restarts do not self-conflict before the runner starts. Fixes #69304. (#69779) Thanks @chinar-amrutkar. - Telegram: keep the polling stall watchdog active even when grammY reports the runner as not running while its task is still pending, so a rebuilt transport cannot leave `getUpdates` silent until a manual gateway restart. Fixes #69064. Thanks @LDLoeb. - Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai. diff --git a/src/commands/gateway-status/helpers.test.ts b/src/commands/gateway-status/helpers.test.ts index a8dd4cf96d5..0083fc91d47 100644 --- a/src/commands/gateway-status/helpers.test.ts +++ b/src/commands/gateway-status/helpers.test.ts @@ -188,6 +188,8 @@ describe("resolveAuthForTarget", () => { it("redacts resolver internals from unresolved SecretRef diagnostics", async () => { await withEnvAsync( { + OPENCLAW_GATEWAY_PASSWORD: undefined, + OPENCLAW_GATEWAY_TOKEN: undefined, MISSING_GATEWAY_TOKEN: undefined, }, async () => { diff --git a/ui/src/styles/chat/grouped.css b/ui/src/styles/chat/grouped.css index 24c2626b91b..c19deb16fee 100644 --- a/ui/src/styles/chat/grouped.css +++ b/ui/src/styles/chat/grouped.css @@ -46,7 +46,9 @@ .chat-group-footer { display: flex; gap: 8px; - align-items: baseline; + row-gap: 5px; + align-items: center; + flex-wrap: wrap; margin-top: 6px; } @@ -60,6 +62,7 @@ font-size: 11px; color: var(--muted); opacity: 0.7; + line-height: 1.2; } /* ── Group footer action buttons (TTS, delete) ── */ @@ -382,14 +385,81 @@ img.chat-avatar { .msg-meta { display: inline-flex; align-items: center; - gap: 8px; + gap: 6px; font-size: 11px; line-height: 1; color: var(--muted); - margin-top: 4px; flex-wrap: wrap; } +.msg-meta__summary { + list-style: none; + display: inline-flex; + align-items: center; + gap: 4px; + min-height: 22px; + padding: 2px 7px 2px 5px; + border: 1px solid var(--border); + border-radius: var(--radius-full); + background: color-mix(in srgb, var(--bg-hover, rgba(255, 255, 255, 0.08)) 65%, transparent); + cursor: pointer; + user-select: none; + transition: + border-color var(--duration-fast) ease-out, + background var(--duration-fast) ease-out, + color var(--duration-fast) ease-out; +} + +.msg-meta__summary::-webkit-details-marker { + display: none; +} + +.msg-meta__summary:hover, +.msg-meta__summary:focus-visible { + border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); + background: var(--bg-hover, rgba(255, 255, 255, 0.08)); + color: var(--fg); +} + +.msg-meta__summary:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.msg-meta__summary-icon { + display: inline-flex; + width: 12px; + height: 12px; + transition: transform 120ms ease-out; +} + +.msg-meta__summary-icon svg { + width: 12px; + height: 12px; + fill: none; + stroke: currentColor; + stroke-width: 2; +} + +.msg-meta[open] .msg-meta__summary-icon { + transform: rotate(90deg); +} + +details.msg-meta:not([open]) .msg-meta__details { + display: none; +} + +.msg-meta__details { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 3px 7px; + border: 1px solid var(--border); + border-radius: var(--radius-full); + background: rgba(255, 255, 255, 0.03); +} + .msg-meta__tokens, .msg-meta__cache, .msg-meta__cost, diff --git a/ui/src/ui/chat/grouped-render.test.ts b/ui/src/ui/chat/grouped-render.test.ts index 8318c0918c7..b566db02330 100644 --- a/ui/src/ui/chat/grouped-render.test.ts +++ b/ui/src/ui/chat/grouped-render.test.ts @@ -5,7 +5,9 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { getSafeLocalStorage } from "../../local-storage.ts"; import type { MessageGroup } from "../types/chat-types.ts"; import { + formatChatTimestampForDisplay, renderMessageGroup, + renderStreamingGroup, resolveAssistantTextAvatar, resetAssistantAttachmentAvailabilityCacheForTest, } from "./grouped-render.ts"; @@ -304,6 +306,10 @@ describe("grouped chat rendering", () => { }, 1_000_000, ); + const meta = cached.querySelector("details.msg-meta"); + expect(meta).not.toBeNull(); + expect(meta?.open).toBe(false); + expect(meta?.querySelector("summary")?.textContent).toContain("Context"); expect(cached.querySelector(".msg-meta__ctx")?.textContent).toBe("44% ctx"); expect(cached.textContent).toContain("R438.4k"); expect(cached.textContent).toContain("W307"); @@ -320,6 +326,34 @@ describe("grouped chat rendering", () => { expect(outputHeavy.querySelector(".msg-meta__ctx")?.textContent).toBe("10% ctx"); }); + it("renders full dates with message timestamps", () => { + const container = document.createElement("div"); + const timestamp = Date.UTC(2026, 3, 24, 18, 30); + + renderAssistantMessage(container, { + role: "assistant", + content: "Done", + timestamp, + }); + + const time = container.querySelector(".chat-group-timestamp"); + const display = formatChatTimestampForDisplay(timestamp); + expect(time).not.toBeNull(); + expect(time?.dateTime).toBe(display.dateTime); + expect(time?.textContent?.trim()).toBe(display.label); + expect(time?.getAttribute("title")).toBe(display.title); + }); + + it("renders full dates with streaming timestamps", () => { + const container = document.createElement("div"); + const timestamp = Date.UTC(2026, 3, 24, 18, 30); + + render(renderStreamingGroup("Working", timestamp), container); + + const time = container.querySelector(".chat-group-timestamp"); + expect(time?.textContent?.trim()).toBe(formatChatTimestampForDisplay(timestamp).label); + }); + it("renders configured local user names and avatar variants", () => { const renderUser = (opts: Partial) => { const container = document.createElement("div"); diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index aa240a6bcf9..429492c7a92 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -49,6 +49,53 @@ type AssistantAttachmentAvailability = const assistantAttachmentAvailabilityCache = new Map(); const ASSISTANT_ATTACHMENT_UNAVAILABLE_RETRY_MS = 5_000; +export type ChatTimestampDisplay = { + label: string; + title: string; + dateTime: string; +}; + +export function formatChatTimestampForDisplay(timestamp: number): ChatTimestampDisplay { + const date = new Date(timestamp); + if (!Number.isFinite(date.getTime())) { + return { + label: "Unknown date", + title: "Unknown date", + dateTime: "", + }; + } + + return { + label: date.toLocaleString([], { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + }), + title: date.toLocaleString([], { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + second: "2-digit", + timeZoneName: "short", + }), + dateTime: date.toISOString(), + }; +} + +function renderChatTimestamp(timestamp: number) { + const display = formatChatTimestampForDisplay(timestamp); + return html` + + `; +} + export function resetAssistantAttachmentAvailabilityCacheForTest() { assistantAttachmentAvailabilityCache.clear(); for (const blobUrl of managedImageBlobUrlResolvedCache.values()) { @@ -238,10 +285,6 @@ export function renderStreamingGroup( basePath?: string, authToken?: string | null, ) { - const timestamp = new Date(startedAt).toLocaleTimeString([], { - hour: "numeric", - minute: "2-digit", - }); const name = assistant?.name ?? "Assistant"; return html` @@ -260,7 +303,7 @@ export function renderStreamingGroup( )}

@@ -316,10 +359,6 @@ export function renderMessageGroup( : normalizedRole === "tool" ? "tool" : "other"; - const timestamp = new Date(group.timestamp).toLocaleTimeString([], { - hour: "numeric", - minute: "2-digit", - }); // Aggregate usage/cost/model across all messages in the group const meta = extractGroupMeta(group, opts.contextWindow ?? null); @@ -365,8 +404,7 @@ export function renderMessageGroup( )}