fix(gateway): preserve restart continuation chat type

This commit is contained in:
Ayaan Zaidi
2026-04-22 21:45:07 +05:30
parent 4ef1c06f9e
commit 486d0ec235
3 changed files with 54 additions and 1 deletions

View File

@@ -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.

View File

@@ -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<ReturnType<typeof mocks.consumeRestartSentinel>>);
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",

View File

@@ -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) {