From 09be7de2b2bed96c57f298e5cb1a51d01503a42f Mon Sep 17 00:00:00 2001 From: VACInc <3279061+VACInc@users.noreply.github.com> Date: Fri, 8 May 2026 06:28:51 -0400 Subject: [PATCH] fix: preserve restart continuation authority --- CHANGELOG.md | 1 + src/gateway/server-restart-sentinel.test.ts | 78 ++++++++++++++++++++- src/gateway/server-restart-sentinel.ts | 13 +++- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 069a532eec1..0e5c9f1aa89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -212,6 +212,7 @@ Docs: https://docs.openclaw.ai - CLI/router: when `openclaw ` does not match a CLI subcommand, check plugin tool manifests first so names like `lcm_recent` get an agent-tool diagnostic instead of the misleading suggestion to add the tool name to `plugins.allow`. Fixes #77214. Thanks @100yenadmin. - QA-lab/parity: bump the live mock-openai parity baseline from `claude-opus-4-6`/`claude-sonnet-4-6` to `claude-opus-4-7`/`claude-sonnet-4-7` and the candidate alt from `gpt-5.4-alt` to `gpt-5.5-alt` in `openclaw-release-checks.yml` and `qa-live-transports-convex.yml`, matching the active Opus 4.7 / GPT-5.5 defaults already used elsewhere on main. Carries forward the surface-bump portion of #74290. Thanks @100yenadmin. - QA-lab/scenarios: raise the `approval-turn-tool-followthrough` per-turn fallback timeouts from 20s/30s to 60s so cold mock-gateway parity runs do not flake on the approval-turn chain. Carries forward the timeout-bump portion of #74290. Thanks @100yenadmin. +- Gateway/restart continuation: treat routed post-reboot agent turns as trusted internal continuations while preserving the original Telegram topic route, so owner-only tools remain available for chained restart workflows after reboot. - Agents/compaction: keep the recent tail after manual `/compact` when Pi returns an empty or no-op compaction summary, preventing blank checkpoints from replacing the live context. - Native commands: handle slash commands before workspace and agent-reply bootstrap so Telegram `/status` and other command-only native replies do not wait behind full agent turn setup. - Telegram/groups: include the recent local chat window and nearby reply-target window as generic inbound context so stale reply ancestry does not overshadow the live group conversation. diff --git a/src/gateway/server-restart-sentinel.test.ts b/src/gateway/server-restart-sentinel.test.ts index f4adaddaac9..75ff4e204b3 100644 --- a/src/gateway/server-restart-sentinel.test.ts +++ b/src/gateway/server-restart-sentinel.test.ts @@ -543,10 +543,16 @@ describe("scheduleRestartSentinelWake", () => { BodyForAgent: "stamped:Reply with exactly: Yay! I did it!", BodyForCommands: "", CommandBody: "", - CommandAuthorized: false, + CommandAuthorized: true, + GatewayClientScopes: ["operator.admin"], + InputProvenance: { + kind: "internal_system", + sourceChannel: "whatsapp", + sourceTool: "restart-sentinel", + }, SessionKey: "agent:main:main", - Provider: "whatsapp", - Surface: "whatsapp", + Provider: "webchat", + Surface: "webchat", OriginatingChannel: "whatsapp", OriginatingTo: "+15550002", MessageThreadId: "thread-42", @@ -600,6 +606,72 @@ describe("scheduleRestartSentinelWake", () => { ); }); + it("authorizes routed agentTurn continuations while preserving Telegram topic routing", async () => { + mocks.readRestartSentinel.mockResolvedValue({ + payload: { + sessionKey: "agent:main:telegram:group:-1003826723328:topic:13757", + ts: 123, + continuation: { + kind: "agentTurn", + message: "continue in topic", + }, + }, + } as unknown as Awaited>); + mocks.parseSessionThreadInfo.mockReturnValue({ + baseSessionKey: "agent:main:telegram:group:-1003826723328", + threadId: "13757", + }); + mocks.loadSessionEntry.mockReturnValue({ + cfg: {}, + entry: { + sessionId: "agent:main:telegram:group:-1003826723328:topic:13757", + updatedAt: 0, + origin: { provider: "telegram", chatType: "group" }, + }, + store: {}, + storePath: "/tmp/sessions.json", + canonicalKey: "agent:main:telegram:group:-1003826723328:topic:13757", + legacyKey: undefined, + }); + mocks.deliveryContextFromSession.mockReturnValue({ + channel: "telegram", + to: "telegram:-1003826723328:topic:13757", + accountId: "default", + threadId: 13757, + }); + mocks.resolveOutboundTarget.mockReturnValue({ + ok: true as const, + to: "telegram:-1003826723328:topic:13757", + }); + + await scheduleRestartSentinelWake({ deps: {} as never }); + + expect(mocks.recordInboundSessionAndDispatchReply).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "telegram", + accountId: "default", + routeSessionKey: "agent:main:telegram:group:-1003826723328:topic:13757", + ctxPayload: expect.objectContaining({ + Body: "continue in topic", + CommandAuthorized: true, + GatewayClientScopes: ["operator.admin"], + InputProvenance: { + kind: "internal_system", + sourceChannel: "telegram", + sourceTool: "restart-sentinel", + }, + Provider: "webchat", + Surface: "webchat", + ChatType: "group", + OriginatingChannel: "telegram", + OriginatingTo: "telegram:-1003826723328:topic:13757", + ExplicitDeliverRoute: true, + MessageThreadId: "13757", + }), + }), + ); + }); + 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 8eb1f9b0a30..e5fe3c47969 100644 --- a/src/gateway/server-restart-sentinel.ts +++ b/src/gateway/server-restart-sentinel.ts @@ -41,6 +41,7 @@ import { deliveryContextFromSession, mergeDeliveryContext, } from "../utils/delivery-context.shared.js"; +import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js"; import { injectTimestamp, timestampOptsFromConfig } from "./server-methods/agent-timestamp.js"; import { loadSessionEntry } from "./session-utils.js"; import { runStartupTasks, type StartupTask } from "./startup-tasks.js"; @@ -287,10 +288,16 @@ async function deliverQueuedSessionDelivery(params: { AccountId: route.accountId, MessageSid: messageId, Timestamp: Date.now(), - Provider: route.channel, - Surface: route.channel, + InputProvenance: { + kind: "internal_system", + sourceChannel: route.channel, + sourceTool: "restart-sentinel", + }, + Provider: INTERNAL_MESSAGE_CHANNEL, + Surface: INTERNAL_MESSAGE_CHANNEL, ChatType: route.chatType, - CommandAuthorized: false, + CommandAuthorized: true, + GatewayClientScopes: ["operator.admin"], ReplyToId: route.replyToId, OriginatingChannel: route.channel, OriginatingTo: route.to,