fix(cron): preserve telegram direct thread inference

This commit is contained in:
Peter Steinberger
2026-04-27 07:57:56 +01:00
parent ca9a04b271
commit f89d0f7c53
3 changed files with 104 additions and 2 deletions

View File

@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
- Cron: add `failureAlert.includeSkipped` and `openclaw cron edit --failure-alert-include-skipped` so persistently skipped jobs can alert without counting skips as execution errors or affecting retry backoff. Fixes #60846. Thanks @slideshow-dingo.
- Cron: invalidate stale pending runtime slots after live or offline `jobs.json` schedule edits, while preserving due slots for formatting-only rewrites. Fixes #27996 and #71607; carries forward #71651. Thanks @xialonglee and @fagnersouza666.
- Cron: keep legacy flat `jobs.json` rows loadable while comparing split-state schedule identities, so old cron stores do not crash before in-memory hydration can normalize them. Thanks @codex.
- Cron/Telegram: preserve direct-chat thread IDs and optional account IDs when inferring reminder delivery from Telegram direct-thread session keys. Fixes #44270; carries forward #44325, #44351, #44412, and #72657. Thanks @RunMintOn, @arkyu2077, @0xsline, and @vincentkoc.
- Cron: omit synthetic `delivery.resolved` errors from `--no-deliver` run records while preserving explicit no-deliver target traces for agent-initiated messages. Fixes #72210; carries forward #72219. Thanks @hatemclawbot-collab and @xydigit-sj.
- Cron: classify isolated runs as errors from structured embedded-run execution-denial metadata, with final-output marker fallback for `SYSTEM_RUN_DENIED`, `INVALID_REQUEST`, and approval-binding refusals, so blocked commands no longer appear green in cron history. Fixes #67172; carries forward #67186. Thanks @oc-gh-dr, @hclsys, and @1yihui.
- Onboarding/GitHub Copilot: add manifest-owned `--github-copilot-token` support for non-interactive setup, including env fallback, tokenRef storage in ref mode, saved-profile reuse, and current Copilot default-model wiring. Refs #50002 and supersedes #50003. Thanks @scottgl9.

View File

@@ -429,6 +429,62 @@ describe("cron tool", () => {
});
});
it("preserves telegram direct-chat thread ids when inferring delivery", async () => {
expect(
await executeAddAndReadDelivery({
callId: "call-telegram-direct-thread",
agentSessionKey: "agent:main:telegram:direct:123456789:thread:123456789:99",
}),
).toEqual({
mode: "announce",
channel: "telegram",
to: "123456789",
threadId: "99",
});
});
it("preserves telegram account ids with direct-chat thread inference", async () => {
expect(
await executeAddAndReadDelivery({
callId: "call-telegram-account-direct-thread",
agentSessionKey: "agent:main:telegram:bot-a:direct:123456789:thread:123456789:99",
}),
).toEqual({
mode: "announce",
channel: "telegram",
to: "123456789",
accountId: "bot-a",
threadId: "99",
});
});
it("preserves legacy telegram dm thread ids when inferring delivery", async () => {
expect(
await executeAddAndReadDelivery({
callId: "call-telegram-dm-thread",
agentSessionKey: "agent:main:telegram:dm:123456789:thread:123456789:99",
}),
).toEqual({
mode: "announce",
channel: "telegram",
to: "123456789",
threadId: "99",
});
});
it("drops mismatched telegram direct-chat thread ids when inferring delivery", async () => {
expect(
await executeAddAndReadDelivery({
callId: "call-telegram-mismatched-direct-thread",
agentSessionKey: "agent:main:telegram:direct:123456789:thread:987654321:99",
}),
).toEqual({
mode: "announce",
channel: "telegram",
to: "123456789",
});
});
it("prefers current delivery context over lowercased session-key targets", async () => {
expect(
await executeAddAndReadDelivery({

View File

@@ -3,11 +3,15 @@ import { loadConfig } from "../../config/config.js";
import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js";
import type { CronDelivery, CronMessageChannel } from "../../cron/types.js";
import { normalizeHttpWebhookUrl } from "../../cron/webhook-url.js";
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
import {
parseAgentSessionKey,
parseThreadSessionSuffix,
} from "../../sessions/session-key-utils.js";
import { extractTextFromChatContent } from "../../shared/chat-content.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { isRecord, truncateUtf16Safe } from "../../utils.js";
import {
@@ -398,12 +402,37 @@ function stripThreadSuffixFromSessionKey(sessionKey: string): string {
return parent ? parent : sessionKey;
}
function resolveTelegramDirectThreadId(params: {
peerId: string;
threadId?: string;
}): string | undefined {
const threadId = normalizeOptionalString(params.threadId);
if (!threadId) {
return undefined;
}
const peerId = normalizeOptionalString(params.peerId);
if (!peerId) {
return undefined;
}
const [threadChatId, ...threadIdParts] = threadId.split(":");
if (threadIdParts.length === 0) {
return threadId;
}
if (normalizeOptionalLowercaseString(threadChatId) !== peerId) {
return undefined;
}
return normalizeOptionalString(threadIdParts.join(":"));
}
function inferDeliveryFromSessionKey(agentSessionKey?: string): CronDelivery | null {
const rawSessionKey = agentSessionKey?.trim();
if (!rawSessionKey) {
return null;
}
const parsed = parseAgentSessionKey(stripThreadSuffixFromSessionKey(rawSessionKey));
const threadSuffix = parseThreadSessionSuffix(rawSessionKey);
const parsed = parseAgentSessionKey(
threadSuffix.baseSessionKey ?? stripThreadSuffixFromSessionKey(rawSessionKey),
);
if (!parsed || !parsed.rest) {
return null;
}
@@ -444,10 +473,26 @@ function inferDeliveryFromSessionKey(agentSessionKey?: string): CronDelivery | n
channel = normalizeOptionalLowercaseString(parts[0]) as CronMessageChannel | undefined;
}
const marker = parts[markerIndex];
const delivery: CronDelivery = { mode: "announce", to: peerId };
if (channel) {
delivery.channel = channel;
}
if (channel === "telegram" && markerIndex === 2) {
const accountId = normalizeOptionalString(parts[1]);
if (accountId) {
delivery.accountId = accountId;
}
}
if (channel === "telegram" && (marker === "direct" || marker === "dm")) {
const threadId = resolveTelegramDirectThreadId({
peerId,
threadId: threadSuffix.threadId,
});
if (threadId) {
delivery.threadId = threadId;
}
}
return delivery;
}