mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 22:04:30 +00:00
fix(telegram): route native topic commands to the active session (#38871)
* fix(telegram): resolve session entry for /stop in forum topics Fixes #38675 - Export normalizeStoreSessionKey from store.ts for reuse - Use it in resolveSessionEntryForKey so topic session keys (lowercase in store) are found when handling /stop - Add test for forum topic session key lookup * fix(telegram): share native topic routing with inbound messages * fix: land telegram topic routing follow-up (#38871) --------- Co-authored-by: xialonglee <li.xialong@xydigit.com>
This commit is contained in:
@@ -356,6 +356,20 @@ describe("abort detection", () => {
|
||||
expect(resolveSessionEntryForKey(undefined, "session-1")).toEqual({});
|
||||
});
|
||||
|
||||
it("resolves Telegram forum topic session when lookup key has different casing than store", () => {
|
||||
// Store normalizes keys to lowercase; caller may pass mixed-case. /stop in topic must find entry.
|
||||
const storeKey = "agent:main:telegram:group:-1001234567890:topic:99";
|
||||
const lookupKey = "Agent:Main:Telegram:Group:-1001234567890:Topic:99";
|
||||
const store = {
|
||||
[storeKey]: { sessionId: "pi-topic-99", updatedAt: 0 },
|
||||
} as Record<string, { sessionId: string; updatedAt: number }>;
|
||||
// Direct lookup fails (store uses lowercase keys); normalization fallback must succeed.
|
||||
expect(store[lookupKey]).toBeUndefined();
|
||||
const result = resolveSessionEntryForKey(store, lookupKey);
|
||||
expect(result.entry?.sessionId).toBe("pi-topic-99");
|
||||
expect(result.key).toBe(storeKey);
|
||||
});
|
||||
|
||||
it("fast-aborts even when text commands are disabled", async () => {
|
||||
const { cfg } = await createAbortConfig({ commandsTextEnabled: false });
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
updateSessionStore,
|
||||
@@ -172,13 +173,22 @@ export function formatAbortReplyText(stoppedSubagents?: number): string {
|
||||
export function resolveSessionEntryForKey(
|
||||
store: Record<string, SessionEntry> | undefined,
|
||||
sessionKey: string | undefined,
|
||||
) {
|
||||
): { entry?: SessionEntry; key?: string; legacyKeys?: string[] } {
|
||||
if (!store || !sessionKey) {
|
||||
return {};
|
||||
}
|
||||
const direct = store[sessionKey];
|
||||
if (direct) {
|
||||
return { entry: direct, key: sessionKey };
|
||||
const resolved = resolveSessionStoreEntry({ store, sessionKey });
|
||||
if (resolved.existing) {
|
||||
return resolved.legacyKeys.length > 0
|
||||
? {
|
||||
entry: resolved.existing,
|
||||
key: resolved.normalizedKey,
|
||||
legacyKeys: resolved.legacyKeys,
|
||||
}
|
||||
: {
|
||||
entry: resolved.existing,
|
||||
key: resolved.normalizedKey,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -301,7 +311,7 @@ export async function tryFastAbortFromMessage(params: {
|
||||
if (targetKey) {
|
||||
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
||||
const store = loadSessionStore(storePath);
|
||||
const { entry, key } = resolveSessionEntryForKey(store, targetKey);
|
||||
const { entry, key, legacyKeys } = resolveSessionEntryForKey(store, targetKey);
|
||||
const resolvedTargetKey = key ?? targetKey;
|
||||
const acpManager = getAcpSessionManager();
|
||||
const acpResolution = acpManager.resolveSession({
|
||||
@@ -340,6 +350,11 @@ export async function tryFastAbortFromMessage(params: {
|
||||
applyAbortCutoffToSessionEntry(entry, abortCutoff);
|
||||
entry.updatedAt = Date.now();
|
||||
store[key] = entry;
|
||||
for (const legacyKey of legacyKeys ?? []) {
|
||||
if (legacyKey !== key) {
|
||||
delete store[legacyKey];
|
||||
}
|
||||
}
|
||||
await updateSessionStore(storePath, (nextStore) => {
|
||||
const nextEntry = nextStore[key] ?? entry;
|
||||
if (!nextEntry) {
|
||||
@@ -349,6 +364,11 @@ export async function tryFastAbortFromMessage(params: {
|
||||
applyAbortCutoffToSessionEntry(nextEntry, abortCutoff);
|
||||
nextEntry.updatedAt = Date.now();
|
||||
nextStore[key] = nextEntry;
|
||||
for (const legacyKey of legacyKeys ?? []) {
|
||||
if (legacyKey !== key) {
|
||||
delete nextStore[legacyKey];
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (abortKey) {
|
||||
setAbortMemory(abortKey, true);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { loadSessionStore, resolveStorePath, type SessionEntry } from "../../config/sessions.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "../../config/sessions.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { fireAndForgetHook } from "../../hooks/fire-and-forget.js";
|
||||
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
||||
@@ -65,7 +70,7 @@ const isInboundAudioContext = (ctx: FinalizedMsgContext): boolean => {
|
||||
return AUDIO_HEADER_RE.test(trimmed);
|
||||
};
|
||||
|
||||
const resolveSessionStoreEntry = (
|
||||
const resolveSessionStoreLookup = (
|
||||
ctx: FinalizedMsgContext,
|
||||
cfg: OpenClawConfig,
|
||||
): {
|
||||
@@ -84,7 +89,7 @@ const resolveSessionStoreEntry = (
|
||||
const store = loadSessionStore(storePath);
|
||||
return {
|
||||
sessionKey,
|
||||
entry: store[sessionKey.toLowerCase()] ?? store[sessionKey],
|
||||
entry: resolveSessionStoreEntry({ store, sessionKey }).existing,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
@@ -164,7 +169,7 @@ export async function dispatchReplyFromConfig(params: {
|
||||
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
|
||||
}
|
||||
|
||||
const sessionStoreEntry = resolveSessionStoreEntry(ctx, cfg);
|
||||
const sessionStoreEntry = resolveSessionStoreLookup(ctx, cfg);
|
||||
const acpDispatchSessionKey = sessionStoreEntry.sessionKey ?? sessionKey;
|
||||
const inboundAudio = isInboundAudioContext(ctx);
|
||||
const sessionTtsAuto = normalizeTtsAutoMode(sessionStoreEntry.entry?.ttsAuto);
|
||||
|
||||
Reference in New Issue
Block a user