Skip to content

Commit 584fa32

Browse files
authored
Fix restart sentinel internal continuations (#88161)
* fix restart sentinel internal continuations * update gateway prompt snapshots * stabilize sandbox browser audit timer tests * drive sandbox audit timeouts deterministically * drive gh-read timeout tests deterministically * drive label-open-issues timeout tests deterministically * document deterministic timeout test timers * test: preserve deterministic timer setup after rebase
1 parent dc4f3b5 commit 584fa32

36 files changed

Lines changed: 333 additions & 211 deletions

src/agents/cli-runner.spawn.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,10 +646,16 @@ describe("runCliAgent spawn path", () => {
646646
runId: "run-claude-channel-wrapper",
647647
messageChannel: "telegram",
648648
messageProvider: "acp",
649+
currentChannelId: "telegram:-100123:topic:42",
650+
currentThreadTs: "42",
651+
currentMessageId: "reply-message-1",
649652
});
650653

651654
expect(params.messageChannel).toBe("telegram");
652655
expect(params.messageProvider).toBe("acp");
656+
expect(params.currentChannelId).toBe("telegram:-100123:topic:42");
657+
expect(params.currentThreadTs).toBe("42");
658+
expect(params.currentMessageId).toBe("reply-message-1");
653659
expect(params.cwd).toBe("/tmp/task-repo");
654660
});
655661

src/agents/cli-runner.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,9 @@ export function buildRunClaudeCliAgentParams(params: RunClaudeCliAgentParams): R
779779
images: params.images,
780780
messageChannel: params.messageChannel,
781781
messageProvider: params.messageProvider,
782+
currentChannelId: params.currentChannelId,
783+
currentThreadTs: params.currentThreadTs,
784+
currentMessageId: params.currentMessageId,
782785
};
783786
}
784787

src/agents/cli-runner/prepare.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ function createTestMcpLoopbackServerConfig(port: number) {
9898
"x-openclaw-agent-id": "${OPENCLAW_MCP_AGENT_ID}",
9999
"x-openclaw-account-id": "${OPENCLAW_MCP_ACCOUNT_ID}",
100100
"x-openclaw-message-channel": "${OPENCLAW_MCP_MESSAGE_CHANNEL}",
101+
"x-openclaw-current-channel-id": "${OPENCLAW_MCP_CURRENT_CHANNEL_ID}",
102+
"x-openclaw-current-thread-ts": "${OPENCLAW_MCP_CURRENT_THREAD_TS}",
103+
"x-openclaw-current-message-id": "${OPENCLAW_MCP_CURRENT_MESSAGE_ID}",
101104
"x-openclaw-inbound-event-kind": "${OPENCLAW_MCP_INBOUND_EVENT_KIND}",
102105
"x-openclaw-source-reply-delivery-mode": "${OPENCLAW_MCP_SOURCE_REPLY_DELIVERY_MODE}",
103106
},
@@ -1147,6 +1150,9 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
11471150
cfg: expect.any(Object),
11481151
sessionKey: "agent:main:test",
11491152
messageProvider: undefined,
1153+
currentChannelId: undefined,
1154+
currentThreadTs: undefined,
1155+
currentMessageId: undefined,
11501156
accountId: undefined,
11511157
inboundEventKind: undefined,
11521158
sourceReplyDeliveryMode: undefined,
@@ -1289,11 +1295,17 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
12891295
config: createCliBackendConfig(),
12901296
currentInboundEventKind: "room_event",
12911297
messageChannel: "telegram",
1298+
currentChannelId: "telegram:-100123:topic:42",
1299+
currentThreadTs: "42",
1300+
currentMessageId: "reply-message-1",
12921301
sourceReplyDeliveryMode: "message_tool_only",
12931302
});
12941303

12951304
expect(context.preparedBackend.env).toMatchObject({
12961305
OPENCLAW_MCP_MESSAGE_CHANNEL: "telegram",
1306+
OPENCLAW_MCP_CURRENT_CHANNEL_ID: "telegram:-100123:topic:42",
1307+
OPENCLAW_MCP_CURRENT_THREAD_TS: "42",
1308+
OPENCLAW_MCP_CURRENT_MESSAGE_ID: "reply-message-1",
12971309
OPENCLAW_MCP_INBOUND_EVENT_KIND: "room_event",
12981310
OPENCLAW_MCP_SOURCE_REPLY_DELIVERY_MODE: "message_tool_only",
12991311
});

src/agents/cli-runner/prepare.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ export async function prepareCliRunContext(
277277
OPENCLAW_MCP_ACCOUNT_ID: params.agentAccountId ?? "",
278278
OPENCLAW_MCP_SESSION_KEY: params.sessionKey ?? "",
279279
OPENCLAW_MCP_MESSAGE_CHANNEL: params.messageChannel ?? params.messageProvider ?? "",
280+
OPENCLAW_MCP_CURRENT_CHANNEL_ID: params.currentChannelId ?? "",
281+
OPENCLAW_MCP_CURRENT_THREAD_TS: params.currentThreadTs ?? "",
282+
OPENCLAW_MCP_CURRENT_MESSAGE_ID:
283+
params.currentMessageId != null ? String(params.currentMessageId) : "",
280284
OPENCLAW_MCP_INBOUND_EVENT_KIND: params.currentInboundEventKind ?? "",
281285
OPENCLAW_MCP_SOURCE_REPLY_DELIVERY_MODE: params.sourceReplyDeliveryMode ?? "",
282286
}
@@ -351,6 +355,9 @@ export async function prepareCliRunContext(
351355
cfg: params.config ?? getRuntimeConfig(),
352356
sessionKey: params.sessionKey ?? "",
353357
messageProvider: params.messageChannel ?? params.messageProvider,
358+
currentChannelId: params.currentChannelId,
359+
currentThreadTs: params.currentThreadTs,
360+
currentMessageId: params.currentMessageId,
354361
accountId: params.agentAccountId,
355362
inboundEventKind: params.currentInboundEventKind,
356363
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,

src/agents/cli-runner/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export type RunCliAgentParams = {
7070
skillsSnapshot?: SkillSnapshot;
7171
messageChannel?: string;
7272
messageProvider?: string;
73+
currentChannelId?: string;
74+
currentThreadTs?: string;
75+
currentMessageId?: string | number;
7376
agentAccountId?: string;
7477
/** Trusted sender identity bit for channel action auth. */
7578
senderIsOwner?: boolean;

src/agents/tools/gateway-tool.test.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,15 @@ describe("gateway tool restart continuation", () => {
137137
expect(parameters.properties?.timeoutMs).toMatchObject({ type: "integer", minimum: 1 });
138138
});
139139

140-
it("instructs agents to use continuationMessage when a restart still needs a reply", async () => {
140+
it("instructs agents to use continuationMessage for internal post-restart work", async () => {
141141
const tool = createGatewayTool();
142142

143-
expect(tool.description).toContain("still owe the user a reply");
143+
expect(tool.description).toContain("post-restart work must continue internally");
144+
expect(tool.description).toContain(
145+
"visible follow-up from that turn must use the message tool",
146+
);
144147
expect(tool.description).toContain("continuationMessage");
145-
expect(tool.description).toContain("do not write restart sentinel files directly");
148+
expect(tool.description).toContain("Do not write restart sentinel files directly");
146149
});
147150

148151
it("writes an agentTurn continuation into the restart sentinel", async () => {
@@ -234,9 +237,7 @@ describe("gateway tool restart continuation", () => {
234237
});
235238
});
236239

237-
it("defaults session-scoped restarts to a success continuation", async () => {
238-
const { DEFAULT_RESTART_SUCCESS_CONTINUATION_MESSAGE } =
239-
await import("../../infra/restart-sentinel.js");
240+
it("does not infer a continuation for session-scoped restarts", async () => {
240241
const tool = createGatewayTool({
241242
agentSessionKey: "agent:main:main",
242243
config: {},
@@ -252,10 +253,7 @@ describe("gateway tool restart continuation", () => {
252253

253254
const payload = requireRestartSentinelPayload();
254255
expect(payload.sessionKey).toBe("agent:main:main");
255-
expect(payload.continuation).toEqual({
256-
kind: "agentTurn",
257-
message: DEFAULT_RESTART_SUCCESS_CONTINUATION_MESSAGE,
258-
});
256+
expect(payload.continuation).toBeNull();
259257
});
260258

261259
it("removes the prepared sentinel when restart emission is rejected", async () => {

src/agents/tools/gateway-tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ export function createGatewayTool(opts?: {
369369
label: "Gateway",
370370
name: "gateway",
371371
description:
372-
"Gateway restart/config/update. Before config edits, use config.schema.lookup with targeted dot path. Prefer config.patch for partial merge; config.apply only full replace. Writes hot-reload or restart as needed. Always pass human `note` for post-restart delivery. If still owe the user a reply, pass one-shot `continuationMessage`; do not write restart sentinel files directly.",
372+
"Gateway restart/config/update. Before config edits, use config.schema.lookup with targeted dot path. Prefer config.patch for partial merge; config.apply only full replace. Writes hot-reload or restart as needed. Always pass human `note` for post-restart delivery. If post-restart work must continue internally, pass one-shot `continuationMessage`; visible follow-up from that turn must use the message tool. Do not write restart sentinel files directly.",
373373
parameters: GatewayToolSchema,
374374
execute: async (_toolCallId, args) => {
375375
const params = args as Record<string, unknown>;

src/auto-reply/reply/agent-runner-execution.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2015,6 +2015,14 @@ export async function runAgentTurnWithFallback(params: {
20152015
originatingChannel: params.followupRun.originatingChannel,
20162016
provider: params.sessionCtx.Provider,
20172017
});
2018+
const cliCurrentThreadId =
2019+
params.followupRun.originatingThreadId ?? params.sessionCtx.MessageThreadId;
2020+
const isRestartSentinelContinuation =
2021+
params.sessionCtx.InputProvenance?.kind === "internal_system" &&
2022+
params.sessionCtx.InputProvenance.sourceTool === "restart-sentinel";
2023+
const cliCurrentMessageId = isRestartSentinelContinuation
2024+
? params.sessionCtx.ReplyToId
2025+
: (params.sessionCtx.MessageSidFull ?? params.sessionCtx.MessageSid);
20182026
const result = await agentTurnTiming.measure("cli_run", () =>
20192027
runCliAgentWithLifecycle({
20202028
runId,
@@ -2107,6 +2115,13 @@ export async function runAgentTurnWithFallback(params: {
21072115
skillsSnapshot: params.followupRun.run.skillsSnapshot,
21082116
messageChannel: params.followupRun.originatingChannel ?? undefined,
21092117
messageProvider: hookMessageProvider,
2118+
currentChannelId:
2119+
params.followupRun.originatingTo ??
2120+
params.sessionCtx.OriginatingTo ??
2121+
params.sessionCtx.To,
2122+
currentThreadTs:
2123+
cliCurrentThreadId != null ? String(cliCurrentThreadId) : undefined,
2124+
currentMessageId: cliCurrentMessageId,
21102125
agentAccountId: params.followupRun.run.agentAccountId,
21112126
senderIsOwner: params.followupRun.run.senderIsOwner,
21122127
disableTools: params.opts?.disableTools,

src/auto-reply/reply/agent-runner-utils.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,63 @@ describe("agent-runner-utils", () => {
309309
expect(context.currentChannelId).toBe("channel:123456789012345678");
310310
expect(context.currentMessageId).toBe("msg-9");
311311
});
312+
313+
it("does not expose restart-sentinel synthetic ids as message-tool reply targets", () => {
314+
hoisted.getChannelPluginMock.mockReturnValue({
315+
threading: {
316+
buildToolContext: ({
317+
context,
318+
}: {
319+
context: { To?: string; MessageThreadId?: string | number };
320+
}) => ({
321+
currentChannelId: context.To,
322+
currentThreadTs:
323+
context.MessageThreadId != null ? String(context.MessageThreadId) : undefined,
324+
}),
325+
},
326+
});
327+
328+
const context = buildThreadingToolContext({
329+
sessionCtx: {
330+
Provider: "webchat",
331+
OriginatingChannel: "telegram",
332+
OriginatingTo: "telegram:-1003841603622:topic:928",
333+
MessageThreadId: 928,
334+
MessageSid: "restart-sentinel:agent:main:telegram:agentTurn:123",
335+
InputProvenance: {
336+
kind: "internal_system",
337+
sourceChannel: "telegram",
338+
sourceTool: "restart-sentinel",
339+
},
340+
},
341+
config: {},
342+
hasRepliedRef: undefined,
343+
});
344+
345+
expect(context.currentChannelId).toBe("telegram:-1003841603622:topic:928");
346+
expect(context.currentThreadTs).toBe("928");
347+
expect(context.currentMessageId).toBeUndefined();
348+
});
349+
350+
it("uses restart-sentinel reply target when one exists", () => {
351+
const context = buildThreadingToolContext({
352+
sessionCtx: {
353+
Provider: "webchat",
354+
OriginatingChannel: "whatsapp",
355+
OriginatingTo: "whatsapp:+15550002",
356+
ReplyToId: "provider-reply-id",
357+
MessageSid: "restart-sentinel:agent:main:whatsapp:agentTurn:123",
358+
InputProvenance: {
359+
kind: "internal_system",
360+
sourceChannel: "whatsapp",
361+
sourceTool: "restart-sentinel",
362+
},
363+
},
364+
config: {},
365+
hasRepliedRef: undefined,
366+
});
367+
368+
expect(context.currentChannelId).toBe("whatsapp:+15550002");
369+
expect(context.currentMessageId).toBe("provider-reply-id");
370+
});
312371
});

src/auto-reply/reply/agent-runner-utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,12 @@ export function buildThreadingToolContext(params: {
105105
hasRepliedRef: { value: boolean } | undefined;
106106
}): ChannelThreadingToolContext {
107107
const { sessionCtx, config, hasRepliedRef } = params;
108-
const currentMessageId = sessionCtx.MessageSidFull ?? sessionCtx.MessageSid;
108+
const isRestartSentinelContinuation =
109+
sessionCtx.InputProvenance?.kind === "internal_system" &&
110+
sessionCtx.InputProvenance.sourceTool === "restart-sentinel";
111+
const currentMessageId = isRestartSentinelContinuation
112+
? sessionCtx.ReplyToId
113+
: (sessionCtx.MessageSidFull ?? sessionCtx.MessageSid);
109114
const originProvider = resolveOriginMessageProvider({
110115
originatingChannel: sessionCtx.OriginatingChannel,
111116
provider: sessionCtx.Provider,

0 commit comments

Comments
 (0)