fix: cover Telegram current session status

This commit is contained in:
Alex Knight
2026-05-04 13:43:09 +10:00
parent abe204f6e0
commit c3c964ecfd
8 changed files with 107 additions and 10 deletions

View File

@@ -462,7 +462,7 @@ jobs:
published_upgrade_survivor_baselines: all-since-2026.4.23
published_upgrade_survivor_scenarios: reported-issues
telegram_mode: mock-openai
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-mention-gating
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-current-session-status-tool,telegram-mention-gating
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}

View File

@@ -134,7 +134,7 @@ Docs: https://docs.openclaw.ai
- Channels/streaming: expose `streaming.progress.label`, `labels`, `maxLines`, and `toolProgress` in bundled channel config metadata so progress draft settings appear in config, docs, and control surfaces. Thanks @vincentkoc.
- Channels/streaming: normalize whitespace and case for `streaming.progress.label: "auto"` so progress draft labels keep using the built-in label pool instead of rendering a literal `auto` title. Thanks @vincentkoc.
- Plugins/Codex: preserve Codex-native OAuth routing for `/codex bind` app-server turns so bound sessions keep the selected Codex auth profile instead of falling back to public OpenAI credentials. (#76714) Thanks @keshavbotagent.
- Fix `session_status` with `sessionKey: "current"` resolving to stale Telegram direct session instead of live run session (#76708). Thanks @amknight.
- Telegram: keep status checks pointed at the active chat so asking for the current session no longer reports an old direct-message conversation. (#76708) Thanks @amknight.
- Gateway/install: prefer supported system Node over nvm/fnm/volta/asdf/mise when regenerating managed gateway services, so `gateway install --force` no longer recreates service definitions that doctor immediately flags as version-manager-backed. Fixes #76339. Thanks @brokemac79.
- Cron/status: render explicit `delivery.mode: "none"` jobs as no-delivery previews and label cron session history distinctly instead of showing fallback delivery or direct-session rows. Fixes #76945.
- Gateway/usage: serve `usage.cost` and `sessions.usage` from a durable transcript aggregate cache with lock-safe background refreshes and localized stale-cache status, so large usage views avoid repeated full scans. (#76650) Thanks @Marvinthebored.

View File

@@ -331,6 +331,7 @@ describe("telegram live qa runtime", () => {
"telegram-tools-compact-command",
"telegram-whoami-command",
"telegram-context-command",
"telegram-current-session-status-tool",
"telegram-mentioned-message-reply",
"telegram-mention-gating",
]);
@@ -340,9 +341,15 @@ describe("telegram live qa runtime", () => {
"telegram-tools-compact-command",
"telegram-whoami-command",
"telegram-context-command",
"telegram-current-session-status-tool",
"telegram-mentioned-message-reply",
"telegram-mention-gating",
]);
expect(
scenarios
.find((scenario) => scenario.id === "telegram-current-session-status-tool")
?.buildRun("sut_bot").expectedTextIncludes,
).toEqual(["QA-TELEGRAM-CURRENT-SESSION-OK", ":telegram:group:"]);
expect(
scenarios
.find((scenario) => scenario.id === "telegram-mentioned-message-reply")

View File

@@ -47,6 +47,7 @@ type TelegramQaScenarioId =
| "telegram-tools-compact-command"
| "telegram-whoami-command"
| "telegram-context-command"
| "telegram-current-session-status-tool"
| "telegram-mentioned-message-reply"
| "telegram-mention-gating";
@@ -208,6 +209,7 @@ type TelegramMessage = {
type TelegramUpdate = {
update_id: number;
edited_message?: TelegramMessage;
message?: TelegramMessage;
};
@@ -270,6 +272,17 @@ const TELEGRAM_QA_SCENARIOS: TelegramQaScenarioDefinition[] = [
expectedTextIncludes: ["/context list", "Inline shortcut"],
}),
},
{
id: "telegram-current-session-status-tool",
title: "Telegram current session_status tool call",
defaultEnabled: false,
timeoutMs: 60_000,
buildRun: (sutUsername) => ({
expectReply: true,
input: `@${sutUsername} Telegram current session_status QA check. Call session_status with sessionKey set to current, then reply with the exact QA marker and resolved session key.`,
expectedTextIncludes: ["QA-TELEGRAM-CURRENT-SESSION-OK", ":telegram:group:"],
}),
},
{
id: "telegram-mentioned-message-reply",
title: "Telegram mentioned message gets a reply",
@@ -471,7 +484,7 @@ function detectMediaKinds(message: TelegramMessage) {
}
function normalizeTelegramObservedMessage(update: TelegramUpdate): TelegramObservedMessage | null {
const message = update.message;
const message = update.message ?? update.edited_message;
if (!message?.from?.id) {
return null;
}
@@ -608,7 +621,7 @@ async function flushTelegramUpdates(token: string) {
{
offset,
timeout: 0,
allowed_updates: ["message"],
allowed_updates: ["message", "edited_message"],
},
15_000,
);
@@ -653,10 +666,12 @@ async function waitForObservedMessage(params: {
observedMessages: TelegramObservedMessage[];
observationScenarioId: string;
observationScenarioTitle: string;
expectedTextIncludes?: string[];
}) {
const startedAt = Date.now();
let offset = params.initialOffset;
let lastPollingError: unknown;
let lastExpectedMismatch: Error | undefined;
while (Date.now() - startedAt < params.timeoutMs) {
const remainingMs = Math.max(
1_000,
@@ -671,7 +686,7 @@ async function waitForObservedMessage(params: {
{
offset,
timeout: timeoutSeconds,
allowed_updates: ["message"],
allowed_updates: ["message", "edited_message"],
},
timeoutSeconds * 1000 + 5_000,
);
@@ -703,10 +718,23 @@ async function waitForObservedMessage(params: {
};
params.observedMessages.push(observedMessage);
if (matchedScenario) {
try {
assertTelegramScenarioReply({
expectedTextIncludes: params.expectedTextIncludes,
message: observedMessage,
});
} catch (error) {
lastExpectedMismatch =
error instanceof Error ? error : new Error(formatErrorMessage(error));
continue;
}
return { message: observedMessage, nextOffset: offset, observedAtMs: batchObservedAtMs };
}
}
}
if (lastExpectedMismatch) {
throw lastExpectedMismatch;
}
const timeoutMessage = `timed out after ${params.timeoutMs}ms waiting for Telegram message`;
if (lastPollingError) {
throw new Error(
@@ -1332,6 +1360,9 @@ export async function runTelegramQaLive(params: {
observedMessages,
observationScenarioId: scenario.id,
observationScenarioTitle: scenario.title,
expectedTextIncludes: scenarioRun.expectReply
? scenarioRun.expectedTextIncludes
: undefined,
predicate: (message) =>
matchesTelegramScenarioReply({
allowAnySutReply: scenarioRun.allowAnySutReply,

View File

@@ -152,6 +152,7 @@ const QA_TOOL_PROGRESS_PROMPT_RE = /tool progress qa check/i;
const QA_GROUP_VISIBLE_REPLY_TOOL_PROMPT_RE = /qa group visible reply tool check/i;
const QA_GROUP_MESSAGE_UNAVAILABLE_FALLBACK_PROMPT_RE =
/qa group message unavailable fallback check/i;
const QA_TELEGRAM_CURRENT_SESSION_STATUS_PROMPT_RE = /telegram current session_status qa check/i;
const QA_SUBAGENT_DIRECT_FALLBACK_PROMPT_RE = /subagent direct fallback qa check/i;
const QA_SUBAGENT_DIRECT_FALLBACK_WORKER_RE = /subagent direct fallback worker/i;
const QA_SUBAGENT_DIRECT_FALLBACK_MARKER = "QA-SUBAGENT-DIRECT-FALLBACK-OK";
@@ -673,6 +674,28 @@ function hasToolErrorOutput(toolJson: Record<string, unknown> | null, toolOutput
return /\b(?:error|failed|failure|not found|no such file|enoent)\b/i.test(toolOutput);
}
function extractSessionStatusSessionKey(
toolJson: Record<string, unknown> | null,
toolOutput: string,
) {
const details = toolJson?.details;
if (details && typeof details === "object") {
const sessionKey = (details as { sessionKey?: unknown }).sessionKey;
if (typeof sessionKey === "string" && sessionKey.trim()) {
return sessionKey.trim();
}
}
const topLevelSessionKey = toolJson?.sessionKey;
if (typeof topLevelSessionKey === "string" && topLevelSessionKey.trim()) {
return topLevelSessionKey.trim();
}
const statusLineSessionKey = /(?:^|\n)[^\n]*Session:\s*([^\s\n]+)/u.exec(toolOutput)?.[1];
if (statusLineSessionKey?.trim()) {
return statusLineSessionKey.trim();
}
return /"sessionKey"\s*:\s*"([^"]+)"/.exec(toolOutput)?.[1]?.trim() ?? "";
}
function isHeartbeatPrompt(text: string) {
const trimmed = text.trim();
if (!trimmed || /remember this fact/i.test(trimmed)) {
@@ -1349,6 +1372,17 @@ async function buildResponsesPayload(
exactMarkerDirective ?? exactReplyDirective ?? "QA-GROUP-FALLBACK-OK",
);
}
if (QA_TELEGRAM_CURRENT_SESSION_STATUS_PROMPT_RE.test(allInputText)) {
if (!toolOutput && hasDeclaredTool(body, "session_status")) {
return buildToolCallEventsWithArgs("session_status", { sessionKey: "current" });
}
const sessionKey = extractSessionStatusSessionKey(toolJson, toolOutput);
return buildAssistantEvents(
sessionKey.includes(":telegram:group:")
? `QA-TELEGRAM-CURRENT-SESSION-OK ${sessionKey}`
: `QA-TELEGRAM-CURRENT-SESSION-BAD ${sessionKey || "missing-session-key"}`,
);
}
if (/\bmarker\b/i.test(allInputText) && exactReplyDirective) {
return buildAssistantEvents(exactReplyDirective);
}

View File

@@ -645,6 +645,29 @@ describe("session_status tool", () => {
expect(details.statusText).toContain("🧠 Model:");
});
it("resolves sandboxed sessionKey=current to the requester when no run session override exists", async () => {
resetSessionStore({});
const tool = getSessionStatusTool("agent:main:telegram:group:-5096326138", {
sandboxed: true,
});
const result = await tool.execute("call-current-sandboxed-channel", {
sessionKey: "current",
});
const details = result.details as { ok?: boolean; sessionKey?: string; statusText?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:telegram:group:-5096326138");
expect(details.statusText).toContain("OpenClaw");
expect(details.statusText).toContain("🧠 Model:");
expect(callGatewayMock).not.toHaveBeenCalledWith(
expect.objectContaining({
method: "sessions.resolve",
params: expect.objectContaining({ key: "current" }),
}),
);
});
it("resolves the default session_status lookup for a channel-plugin requester via implicit fallback", async () => {
resetSessionStore({});

View File

@@ -364,9 +364,11 @@ export function createSessionStatusTool(opts?: {
}),
);
// Resolve "current" to the live run session key for lookup purposes (#76708).
if (requestedKeyRaw === "current" && opts?.runSessionKey) {
requestedKeyRaw = opts.runSessionKey;
// Resolve semantic "current" to the live run session key for lookup purposes (#76708).
// In sandboxed channel runs there may be no separate runSessionKey because the sandbox
// key already is the live requester; avoid probing literal "current" through the gateway.
if (requestedKeyRaw === "current" && (opts?.runSessionKey || opts?.sandboxed === true)) {
requestedKeyRaw = opts.runSessionKey ?? effectiveRequesterKey;
}
const currentSessionAlias = resolveCurrentSessionClientAlias({
@@ -508,7 +510,7 @@ export function createSessionStatusTool(opts?: {
if (!resolved) {
const fallback = resolveImplicitCurrentSessionFallback({
allowFallback: requestedKeyRaw === "current" || requestedKeyParam === undefined,
allowFallback: isSemanticCurrentRequest || requestedKeyParam === undefined,
storeScopedRequesterKey,
});
if (fallback) {

View File

@@ -522,7 +522,7 @@ describe("package artifact reuse", () => {
expect(workflow).toContain("published_upgrade_survivor_scenarios: reported-issues");
expect(workflow).toContain("telegram_mode: mock-openai");
expect(workflow).toContain(
"telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-mention-gating",
"telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-current-session-status-tool,telegram-mention-gating",
);
expect(workflow).toContain("ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}");
expect(workflow).toContain("ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}");