diff --git a/CHANGELOG.md b/CHANGELOG.md index d501bd8c872..3a7bfad51dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index 95d5e1bf97a..38194e00c37 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -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({ diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 0f516911871..8d6d8b1dec1 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -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; }