diff --git a/CHANGELOG.md b/CHANGELOG.md index fb9d2bc1a47..38c29cff9a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/restart: preserve group and channel chat context when resuming an agent turn after a Gateway restart, so continuation replies keep the same prompt, routing, and tool-status behavior as the original conversation. - Gateway/pairing: shared-secret loopback CLI clients now silently auto-approve `metadata-upgrade` pairing (platform / device family refresh) instead of being disconnected with `1008 pairing required`. This matches the scope-upgrade and role-upgrade behavior added in #69431 and unblocks non-interactive CLI automation when a paired-device record has a stale platform string (e.g. device key replicated across hosts, install migrated between OSes, or platform-string format changed between OpenClaw versions). Browser / Control-UI clients keep the existing approval-required flow for metadata changes. - Gateway/pairing: treat any forwarded-header evidence (`Forwarded`, `X-Forwarded-*`, or `X-Real-IP`) as proxied WebSocket traffic before pairing locality checks, so reverse-proxy topologies cannot use the loopback shared-secret helper auto-pairing path. - Gateway/pairing webchat: render `/pair qr` replies as structured media instead of raw markdown text, preserve inline reply threading and silent-control handling on media replies, avoid persisting sensitive QR images into transcript history, and keep local webchat media embedding behind internal-only trust markers. (#70047) Thanks @BunsDev. diff --git a/src/gateway/server-restart-sentinel.test.ts b/src/gateway/server-restart-sentinel.test.ts index 006038327c4..76c3729dce1 100644 --- a/src/gateway/server-restart-sentinel.test.ts +++ b/src/gateway/server-restart-sentinel.test.ts @@ -432,6 +432,51 @@ describe("scheduleRestartSentinelWake", () => { ); }); + it("preserves the session chat type for agentTurn continuations", async () => { + mocks.consumeRestartSentinel.mockResolvedValue({ + payload: { + sessionKey: "agent:main:group", + deliveryContext: { + channel: "telegram", + to: "telegram:-1001", + accountId: "default", + }, + ts: 123, + continuation: { + kind: "agentTurn", + message: "continue", + }, + }, + } as Awaited>); + mocks.loadSessionEntry.mockReturnValue({ + cfg: {}, + entry: { + sessionId: "agent:main:group", + updatedAt: 0, + origin: { provider: "telegram", chatType: "group" }, + }, + store: {}, + storePath: "/tmp/sessions.json", + canonicalKey: "agent:main:group", + legacyKey: undefined, + }); + mocks.resolveOutboundTarget.mockReturnValue({ ok: true as const, to: "telegram:-1001" }); + + await scheduleRestartSentinelWake({ deps: {} as never }); + + expect(mocks.recordInboundSessionAndDispatchReply).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "telegram", + routeSessionKey: "agent:main:group", + ctxPayload: expect.objectContaining({ + ChatType: "group", + OriginatingChannel: "telegram", + OriginatingTo: "telegram:-1001", + }), + }), + ); + }); + it("preserves derived reply transport ids in continuation context", async () => { mocks.getChannelPlugin.mockReturnValue({ id: "whatsapp", diff --git a/src/gateway/server-restart-sentinel.ts b/src/gateway/server-restart-sentinel.ts index bcf93796856..0b871e3c775 100644 --- a/src/gateway/server-restart-sentinel.ts +++ b/src/gateway/server-restart-sentinel.ts @@ -1,6 +1,7 @@ import { resolveSessionAgentId } from "../agents/agent-scope.js"; import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; +import type { ChatType } from "../channels/chat-type.js"; import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js"; import { recordInboundSession } from "../channels/session.js"; import type { CliDeps } from "../cli/deps.types.js"; @@ -149,6 +150,7 @@ type RestartContinuationRoute = { accountId?: string; replyToId?: string; threadId?: string; + chatType: ChatType; }; function resolveRestartContinuationRoute(params: { @@ -157,6 +159,7 @@ function resolveRestartContinuationRoute(params: { accountId?: string; replyToId?: string; threadId?: string; + chatType: ChatType; }): RestartContinuationRoute | undefined { if (!params.channel || !params.to) { return undefined; @@ -167,6 +170,7 @@ function resolveRestartContinuationRoute(params: { ...(params.accountId ? { accountId: params.accountId } : {}), ...(params.replyToId ? { replyToId: params.replyToId } : {}), ...(params.threadId ? { threadId: params.threadId } : {}), + chatType: params.chatType, }; } @@ -246,7 +250,7 @@ async function dispatchRestartSentinelContinuation(params: { Timestamp: Date.now(), Provider: route.channel, Surface: route.channel, - ChatType: "direct", + ChatType: route.chatType, CommandAuthorized: true, ReplyToId: route.replyToId, OriginatingChannel: route.channel, @@ -337,12 +341,14 @@ async function loadRestartSentinelStartupTask(params: { const sentinelContext = payload.deliveryContext; let sessionDeliveryContext = deliveryContextFromSession(entry); + let chatType = entry?.origin?.chatType ?? "direct"; if ( !hasRoutableDeliveryContext(sessionDeliveryContext) && baseSessionKey && baseSessionKey !== sessionKey ) { const { entry: baseEntry } = loadSessionEntry(baseSessionKey); + chatType = entry?.origin?.chatType ?? baseEntry?.origin?.chatType ?? "direct"; sessionDeliveryContext = mergeDeliveryContext( sessionDeliveryContext, deliveryContextFromSession(baseEntry), @@ -426,6 +432,7 @@ async function loadRestartSentinelStartupTask(params: { accountId: origin?.accountId, replyToId, threadId: resolvedThreadId, + chatType, }), }); } catch (err) {