mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
fix(discord): inherit thread model overrides without transcript fork
This commit is contained in:
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Feishu/Windows: normalize bundled channel sidecar loads before Jiti evaluates them, so Feishu outbound sends no longer fail with raw `C:` ESM loader errors on Windows. Fixes #72783. Thanks @jackychen-png.
|
||||
- Agents/tools: ignore volatile `exec` runtime metadata when comparing tool-loop outcomes, so enabled loop detection can stop repeated identical shell-command results instead of resetting on duration, PID, session, or cwd changes. Fixes #34574; supersedes #41502. Thanks @gucasbrg and @Zcg2021.
|
||||
- Agents/fallback: classify internal live-session model switch conflicts as unknown fallback failures instead of provider overloads, preventing local vLLM endpoints from receiving misleading overloaded cooldowns. Refs #63229. Thanks @clawdia-lobster.
|
||||
- Discord: let thread sessions inherit the parent channel's session-level `/model` override as a model-only fallback without enabling parent transcript inheritance. Fixes #72755. Thanks @solavrc.
|
||||
- Control UI: keep session-specific assistant identity loads authoritative after WebSocket connect, so non-main agent chat sessions do not show the main agent name in the header after bootstrap refreshes. Fixes #72776. Thanks @rockytian-top.
|
||||
- Agents/Qwen: preserve exact custom `modelstudio` provider configs with foreign `api` owners so explicit OpenAI-compatible Model Studio endpoints no longer get normalized into the bundled Qwen plugin path. Fixes #64483. Thanks @FiredMosquito831.
|
||||
- MCP/bundle-mcp: normalize CLI-native `type: "http"` MCP server entries to OpenClaw `transport: "streamable-http"` on save, repair existing configs with doctor, and keep embedded Pi from falling back to legacy SSE GET-first startup for those servers. Fixes #72757. Thanks @Studioscale.
|
||||
|
||||
@@ -585,6 +585,7 @@ Default slash command settings:
|
||||
Thread behavior:
|
||||
|
||||
- Discord threads route as channel sessions and inherit parent channel config unless overridden.
|
||||
- Thread sessions inherit the parent channel's session-level `/model` selection as a model-only fallback; thread-local `/model` selections still take precedence and parent transcript history is not copied unless transcript inheritance is enabled.
|
||||
- `channels.discord.thread.inheritParent` (default `false`) opts new auto-threads into seeding from the parent transcript. Per-account overrides live under `channels.discord.accounts.<id>.thread.inheritParent`.
|
||||
- Message-tool reactions can resolve `user:<id>` DM targets.
|
||||
- `guilds.<guild>.channels.<channel>.requireMention: false` is preserved during reply-stage activation fallback.
|
||||
|
||||
@@ -290,6 +290,8 @@ function getLastDispatchCtx():
|
||||
CommandBody?: string;
|
||||
MediaTranscribedIndexes?: number[];
|
||||
MessageThreadId?: string | number;
|
||||
ModelParentSessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
SessionKey?: string;
|
||||
Transcript?: string;
|
||||
}
|
||||
@@ -302,6 +304,8 @@ function getLastDispatchCtx():
|
||||
CommandBody?: string;
|
||||
MediaTranscribedIndexes?: number[];
|
||||
MessageThreadId?: string | number;
|
||||
ModelParentSessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
SessionKey?: string;
|
||||
Transcript?: string;
|
||||
};
|
||||
@@ -788,6 +792,35 @@ describe("processDiscordMessage session routing", () => {
|
||||
accountId: "default",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes Discord thread parent only for model inheritance when transcript inheritance is off", async () => {
|
||||
const ctx = await createBaseContext({
|
||||
baseSessionKey: "agent:main:discord:channel:thread-1",
|
||||
route: {
|
||||
...BASE_CHANNEL_ROUTE,
|
||||
sessionKey: "agent:main:discord:channel:thread-1",
|
||||
},
|
||||
messageChannelId: "thread-1",
|
||||
message: {
|
||||
id: "m1",
|
||||
channelId: "thread-1",
|
||||
timestamp: new Date().toISOString(),
|
||||
attachments: [],
|
||||
},
|
||||
threadChannel: { id: "thread-1", name: "child-thread" },
|
||||
threadParentId: "parent-1",
|
||||
discordConfig: { thread: { inheritParent: false } },
|
||||
});
|
||||
|
||||
await processDiscordMessage(ctx as any);
|
||||
|
||||
expect(getLastDispatchCtx()).toMatchObject({
|
||||
SessionKey: "agent:main:discord:channel:thread-1",
|
||||
MessageThreadId: "thread-1",
|
||||
ModelParentSessionKey: "agent:main:discord:channel:parent-1",
|
||||
});
|
||||
expect(getLastDispatchCtx()?.ParentSessionKey).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("processDiscordMessage draft streaming", () => {
|
||||
|
||||
@@ -384,6 +384,7 @@ export async function processDiscordMessage(
|
||||
let threadStarterBody: string | undefined;
|
||||
let threadLabel: string | undefined;
|
||||
let parentSessionKey: string | undefined;
|
||||
let modelParentSessionKey: string | undefined;
|
||||
if (threadChannel) {
|
||||
const includeThreadStarter = channelConfig?.includeThreadStarter !== false;
|
||||
if (includeThreadStarter) {
|
||||
@@ -423,6 +424,7 @@ export async function processDiscordMessage(
|
||||
channel: route.channel,
|
||||
peer: { kind: "channel", id: threadParentId },
|
||||
});
|
||||
modelParentSessionKey = parentSessionKey;
|
||||
}
|
||||
if (!threadParentInheritanceEnabled) {
|
||||
parentSessionKey = undefined;
|
||||
@@ -522,6 +524,8 @@ export async function processDiscordMessage(
|
||||
ReplyToBody: filteredReplyContext?.body,
|
||||
ReplyToSender: filteredReplyContext?.sender,
|
||||
ParentSessionKey: autoThreadContext?.ParentSessionKey ?? threadKeys.parentSessionKey,
|
||||
ModelParentSessionKey:
|
||||
autoThreadContext?.ModelParentSessionKey ?? modelParentSessionKey ?? undefined,
|
||||
MessageThreadId: threadChannel?.id ?? autoThreadContext?.createdThreadId ?? undefined,
|
||||
ThreadStarterBody: threadStarterBody,
|
||||
ThreadLabel: threadLabel,
|
||||
|
||||
@@ -292,6 +292,11 @@ describe("resolveDiscordAutoThreadContext", () => {
|
||||
createdThreadId: "thread",
|
||||
expectedNull: false,
|
||||
parentInheritanceEnabled: false,
|
||||
expectedModelParentSessionKey: buildAgentSessionKey({
|
||||
agentId: "agent",
|
||||
channel: "discord",
|
||||
peer: { kind: "channel", id: "parent" },
|
||||
}),
|
||||
expectedParentSessionKey: undefined,
|
||||
},
|
||||
{
|
||||
@@ -299,6 +304,11 @@ describe("resolveDiscordAutoThreadContext", () => {
|
||||
createdThreadId: "thread",
|
||||
expectedNull: false,
|
||||
parentInheritanceEnabled: true,
|
||||
expectedModelParentSessionKey: buildAgentSessionKey({
|
||||
agentId: "agent",
|
||||
channel: "discord",
|
||||
peer: { kind: "channel", id: "parent" },
|
||||
}),
|
||||
expectedParentSessionKey: buildAgentSessionKey({
|
||||
agentId: "agent",
|
||||
channel: "discord",
|
||||
@@ -333,6 +343,9 @@ describe("resolveDiscordAutoThreadContext", () => {
|
||||
}),
|
||||
);
|
||||
expect(context?.ParentSessionKey, testCase.name).toBe(testCase.expectedParentSessionKey);
|
||||
expect(context?.ModelParentSessionKey, testCase.name).toBe(
|
||||
testCase.expectedModelParentSessionKey,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -511,6 +524,11 @@ describe("resolveDiscordAutoThreadReplyPlan", () => {
|
||||
channel: "discord",
|
||||
peer: { kind: "channel", id: "thread" },
|
||||
}),
|
||||
expectedModelParentSessionKey: buildAgentSessionKey({
|
||||
agentId: "agent",
|
||||
channel: "discord",
|
||||
peer: { kind: "channel", id: "parent" },
|
||||
}),
|
||||
expectedParentSessionKey: undefined,
|
||||
},
|
||||
{
|
||||
@@ -525,6 +543,11 @@ describe("resolveDiscordAutoThreadReplyPlan", () => {
|
||||
channel: "discord",
|
||||
peer: { kind: "channel", id: "thread" },
|
||||
}),
|
||||
expectedModelParentSessionKey: buildAgentSessionKey({
|
||||
agentId: "agent",
|
||||
channel: "discord",
|
||||
peer: { kind: "channel", id: "parent" },
|
||||
}),
|
||||
expectedParentSessionKey: buildAgentSessionKey({
|
||||
agentId: "agent",
|
||||
channel: "discord",
|
||||
@@ -566,6 +589,9 @@ describe("resolveDiscordAutoThreadReplyPlan", () => {
|
||||
expect(plan.autoThreadContext?.ParentSessionKey, testCase.name).toBe(
|
||||
testCase.expectedParentSessionKey,
|
||||
);
|
||||
expect(plan.autoThreadContext?.ModelParentSessionKey, testCase.name).toBe(
|
||||
testCase.expectedModelParentSessionKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -389,6 +389,7 @@ export type DiscordAutoThreadContext = {
|
||||
To: string;
|
||||
OriginatingTo: string;
|
||||
SessionKey: string;
|
||||
ModelParentSessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
};
|
||||
|
||||
@@ -413,14 +414,11 @@ export function resolveDiscordAutoThreadContext(params: {
|
||||
channel: params.channel,
|
||||
peer: { kind: "channel", id: createdThreadId },
|
||||
});
|
||||
const parentSessionKey =
|
||||
params.parentInheritanceEnabled === true
|
||||
? buildAgentSessionKey({
|
||||
agentId: params.agentId,
|
||||
channel: params.channel,
|
||||
peer: { kind: "channel", id: messageChannelId },
|
||||
})
|
||||
: undefined;
|
||||
const parentSessionKey = buildAgentSessionKey({
|
||||
agentId: params.agentId,
|
||||
channel: params.channel,
|
||||
peer: { kind: "channel", id: messageChannelId },
|
||||
});
|
||||
|
||||
return {
|
||||
createdThreadId,
|
||||
@@ -428,7 +426,8 @@ export function resolveDiscordAutoThreadContext(params: {
|
||||
To: `channel:${createdThreadId}`,
|
||||
OriginatingTo: `channel:${createdThreadId}`,
|
||||
SessionKey: threadSessionKey,
|
||||
...(parentSessionKey ? { ParentSessionKey: parentSessionKey } : {}),
|
||||
ModelParentSessionKey: parentSessionKey,
|
||||
...(params.parentInheritanceEnabled === true ? { ParentSessionKey: parentSessionKey } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -480,7 +480,8 @@ export async function resolveReplyDirectives(params: {
|
||||
sessionEntry: targetSessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
parentSessionKey: targetSessionEntry?.parentSessionKey ?? ctx.ParentSessionKey,
|
||||
parentSessionKey:
|
||||
targetSessionEntry?.parentSessionKey ?? ctx.ModelParentSessionKey ?? ctx.ParentSessionKey,
|
||||
storePath,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
|
||||
@@ -328,7 +328,7 @@ export async function getReplyFromConfig(
|
||||
groupChannel:
|
||||
sessionEntry.groupChannel ?? sessionCtx.GroupChannel ?? finalized.GroupChannel,
|
||||
groupSubject: sessionEntry.subject ?? sessionCtx.GroupSubject ?? finalized.GroupSubject,
|
||||
parentSessionKey: sessionCtx.ParentSessionKey,
|
||||
parentSessionKey: sessionCtx.ModelParentSessionKey ?? sessionCtx.ParentSessionKey,
|
||||
})
|
||||
: null;
|
||||
const hasSessionModelOverride = Boolean(
|
||||
@@ -339,7 +339,10 @@ export async function getReplyFromConfig(
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
parentSessionKey: sessionEntry.parentSessionKey ?? sessionCtx.ParentSessionKey,
|
||||
parentSessionKey:
|
||||
sessionEntry.parentSessionKey ??
|
||||
sessionCtx.ModelParentSessionKey ??
|
||||
sessionCtx.ParentSessionKey,
|
||||
defaultProvider,
|
||||
});
|
||||
if (storedModelOverride?.model && !hasResolvedHeartbeatModelOverride) {
|
||||
|
||||
@@ -70,6 +70,12 @@ export type MsgContext = {
|
||||
/** Provider account id (multi-account). */
|
||||
AccountId?: string;
|
||||
ParentSessionKey?: string;
|
||||
/**
|
||||
* Session key used only for inheriting session-scoped model/provider
|
||||
* overrides. Unlike ParentSessionKey, this must not trigger transcript
|
||||
* forking or parent-session lifecycle behavior.
|
||||
*/
|
||||
ModelParentSessionKey?: string;
|
||||
MessageSid?: string;
|
||||
/** Provider-specific full message id when MessageSid is a shortened alias. */
|
||||
MessageSidFull?: string;
|
||||
|
||||
Reference in New Issue
Block a user