refactor(agent): dedupe harness and command workflows

This commit is contained in:
Peter Steinberger
2026-02-16 14:52:09 +00:00
parent 04892ee230
commit f717a13039
204 changed files with 7366 additions and 11540 deletions

View File

@@ -38,6 +38,27 @@ vi.mock("../../agents/subagent-registry.js", () => ({
}));
describe("abort detection", () => {
async function runStopCommand(params: {
cfg: OpenClawConfig;
sessionKey: string;
from: string;
to: string;
}) {
return tryFastAbortFromMessage({
ctx: buildTestCtx({
CommandBody: "/stop",
RawBody: "/stop",
CommandAuthorized: true,
SessionKey: params.sessionKey,
Provider: "telegram",
Surface: "telegram",
From: params.from,
To: params.to,
}),
cfg: params.cfg,
});
}
afterEach(() => {
resetAbortMemoryForTest();
});
@@ -109,18 +130,11 @@ describe("abort detection", () => {
const storePath = path.join(root, "sessions.json");
const cfg = { session: { store: storePath }, commands: { text: false } } as OpenClawConfig;
const result = await tryFastAbortFromMessage({
ctx: buildTestCtx({
CommandBody: "/stop",
RawBody: "/stop",
CommandAuthorized: true,
SessionKey: "telegram:123",
Provider: "telegram",
Surface: "telegram",
From: "telegram:123",
To: "telegram:123",
}),
const result = await runStopCommand({
cfg,
sessionKey: "telegram:123",
from: "telegram:123",
to: "telegram:123",
});
expect(result.handled).toBe(true);
@@ -172,18 +186,11 @@ describe("abort detection", () => {
);
expect(getFollowupQueueDepth(sessionKey)).toBe(1);
const result = await tryFastAbortFromMessage({
ctx: buildTestCtx({
CommandBody: "/stop",
RawBody: "/stop",
CommandAuthorized: true,
SessionKey: sessionKey,
Provider: "telegram",
Surface: "telegram",
From: "telegram:123",
To: "telegram:123",
}),
const result = await runStopCommand({
cfg,
sessionKey,
from: "telegram:123",
to: "telegram:123",
});
expect(result.handled).toBe(true);
@@ -229,18 +236,11 @@ describe("abort detection", () => {
},
]);
const result = await tryFastAbortFromMessage({
ctx: buildTestCtx({
CommandBody: "/stop",
RawBody: "/stop",
CommandAuthorized: true,
SessionKey: sessionKey,
Provider: "telegram",
Surface: "telegram",
From: "telegram:parent",
To: "telegram:parent",
}),
const result = await runStopCommand({
cfg,
sessionKey,
from: "telegram:parent",
to: "telegram:parent",
});
expect(result.stoppedSubagents).toBe(1);
@@ -307,18 +307,11 @@ describe("abort detection", () => {
])
.mockReturnValueOnce([]);
const result = await tryFastAbortFromMessage({
ctx: buildTestCtx({
CommandBody: "/stop",
RawBody: "/stop",
CommandAuthorized: true,
SessionKey: sessionKey,
Provider: "telegram",
Surface: "telegram",
From: "telegram:parent",
To: "telegram:parent",
}),
const result = await runStopCommand({
cfg,
sessionKey,
from: "telegram:parent",
to: "telegram:parent",
});
// Should stop both depth-1 and depth-2 agents (cascade)
@@ -389,18 +382,11 @@ describe("abort detection", () => {
])
.mockReturnValueOnce([]);
const result = await tryFastAbortFromMessage({
ctx: buildTestCtx({
CommandBody: "/stop",
RawBody: "/stop",
CommandAuthorized: true,
SessionKey: sessionKey,
Provider: "telegram",
Surface: "telegram",
From: "telegram:parent",
To: "telegram:parent",
}),
const result = await runStopCommand({
cfg,
sessionKey,
from: "telegram:parent",
to: "telegram:parent",
});
// Should skip killing the ended depth-1 run itself, but still kill depth-2.

View File

@@ -11,28 +11,18 @@ import { createOpenClawTools } from "../../agents/openclaw-tools.js";
import { getChannelDock } from "../../channels/dock.js";
import { logVerbose } from "../../globals.js";
import { resolveGatewayMessageChannel } from "../../utils/message-channel.js";
import { listChatCommands } from "../commands-registry.js";
import { listSkillCommandsForWorkspace, resolveSkillCommandInvocation } from "../skill-commands.js";
import {
listReservedChatSlashCommandNames,
listSkillCommandsForWorkspace,
resolveSkillCommandInvocation,
} from "../skill-commands.js";
import { getAbortMemory } from "./abort.js";
import { buildStatusReply, handleCommands } from "./commands.js";
import { isDirectiveOnly } from "./directive-handling.js";
import { extractInlineSimpleCommand } from "./reply-inline.js";
const builtinSlashCommands = (() => {
const reserved = new Set<string>();
for (const command of listChatCommands()) {
if (command.nativeName) {
reserved.add(command.nativeName.toLowerCase());
}
for (const alias of command.textAliases) {
const trimmed = alias.trim();
if (!trimmed.startsWith("/")) {
continue;
}
reserved.add(trimmed.slice(1).toLowerCase());
}
}
for (const name of [
return listReservedChatSlashCommandNames([
"think",
"verbose",
"reasoning",
@@ -41,10 +31,7 @@ const builtinSlashCommands = (() => {
"model",
"status",
"queue",
]) {
reserved.add(name);
}
return reserved;
]);
})();
function resolveSlashCommandName(commandBodyNormalized: string): string | null {

View File

@@ -44,6 +44,30 @@ describe("createModelSelectionState parent inheritance", () => {
});
}
async function resolveHeartbeatStoredOverrideState(hasResolvedHeartbeatModelOverride: boolean) {
const cfg = {} as OpenClawConfig;
const sessionKey = "agent:main:discord:channel:c1";
const sessionEntry = makeEntry({
providerOverride: "openai",
modelOverride: "gpt-4o",
});
const sessionStore = { [sessionKey]: sessionEntry };
return createModelSelectionState({
cfg,
agentCfg: cfg.agents?.defaults,
sessionEntry,
sessionStore,
sessionKey,
defaultProvider,
defaultModel,
provider: "anthropic",
model: "claude-opus-4-5",
hasModelDirective: false,
hasResolvedHeartbeatModelOverride,
});
}
it("inherits parent override from explicit parentSessionKey", async () => {
const cfg = {} as OpenClawConfig;
const parentKey = "agent:main:discord:channel:c1";
@@ -157,58 +181,14 @@ describe("createModelSelectionState parent inheritance", () => {
});
it("applies stored override when heartbeat override was not resolved", async () => {
const cfg = {} as OpenClawConfig;
const sessionKey = "agent:main:discord:channel:c1";
const sessionEntry = makeEntry({
providerOverride: "openai",
modelOverride: "gpt-4o",
});
const sessionStore = {
[sessionKey]: sessionEntry,
};
const state = await createModelSelectionState({
cfg,
agentCfg: cfg.agents?.defaults,
sessionEntry,
sessionStore,
sessionKey,
defaultProvider,
defaultModel,
provider: "anthropic",
model: "claude-opus-4-5",
hasModelDirective: false,
hasResolvedHeartbeatModelOverride: false,
});
const state = await resolveHeartbeatStoredOverrideState(false);
expect(state.provider).toBe("openai");
expect(state.model).toBe("gpt-4o");
});
it("skips stored override when heartbeat override was resolved", async () => {
const cfg = {} as OpenClawConfig;
const sessionKey = "agent:main:discord:channel:c1";
const sessionEntry = makeEntry({
providerOverride: "openai",
modelOverride: "gpt-4o",
});
const sessionStore = {
[sessionKey]: sessionEntry,
};
const state = await createModelSelectionState({
cfg,
agentCfg: cfg.agents?.defaults,
sessionEntry,
sessionStore,
sessionKey,
defaultProvider,
defaultModel,
provider: "anthropic",
model: "claude-opus-4-5",
hasModelDirective: false,
hasResolvedHeartbeatModelOverride: true,
});
const state = await resolveHeartbeatStoredOverrideState(true);
expect(state.provider).toBe("anthropic");
expect(state.model).toBe("claude-opus-4-5");
@@ -219,16 +199,12 @@ describe("createModelSelectionState respects session model override", () => {
const defaultProvider = "inferencer";
const defaultModel = "deepseek-v3-4bit-mlx";
it("applies session modelOverride when set", async () => {
async function resolveState(sessionEntry: ReturnType<typeof makeEntry>) {
const cfg = {} as OpenClawConfig;
const sessionKey = "agent:main:main";
const sessionEntry = makeEntry({
providerOverride: "kimi-coding",
modelOverride: "k2p5",
});
const sessionStore = { [sessionKey]: sessionEntry };
const state = await createModelSelectionState({
return createModelSelectionState({
cfg,
agentCfg: undefined,
sessionEntry,
@@ -240,29 +216,22 @@ describe("createModelSelectionState respects session model override", () => {
model: defaultModel,
hasModelDirective: false,
});
}
it("applies session modelOverride when set", async () => {
const state = await resolveState(
makeEntry({
providerOverride: "kimi-coding",
modelOverride: "k2p5",
}),
);
expect(state.provider).toBe("kimi-coding");
expect(state.model).toBe("k2p5");
});
it("falls back to default when no modelOverride is set", async () => {
const cfg = {} as OpenClawConfig;
const sessionKey = "agent:main:main";
const sessionEntry = makeEntry();
const sessionStore = { [sessionKey]: sessionEntry };
const state = await createModelSelectionState({
cfg,
agentCfg: undefined,
sessionEntry,
sessionStore,
sessionKey,
defaultProvider,
defaultModel,
provider: defaultProvider,
model: defaultModel,
hasModelDirective: false,
});
const state = await resolveState(makeEntry());
expect(state.provider).toBe(defaultProvider);
expect(state.model).toBe(defaultModel);
@@ -270,54 +239,26 @@ describe("createModelSelectionState respects session model override", () => {
it("respects modelOverride even when session model field differs", async () => {
// From issue #14783: stored override should beat last-used fallback model.
const cfg = {} as OpenClawConfig;
const sessionKey = "agent:main:main";
const sessionEntry = makeEntry({
model: "k2p5",
modelProvider: "kimi-coding",
contextTokens: 262_000,
providerOverride: "anthropic",
modelOverride: "claude-opus-4-5",
});
const sessionStore = { [sessionKey]: sessionEntry };
const state = await createModelSelectionState({
cfg,
agentCfg: undefined,
sessionEntry,
sessionStore,
sessionKey,
defaultProvider,
defaultModel,
provider: defaultProvider,
model: defaultModel,
hasModelDirective: false,
});
const state = await resolveState(
makeEntry({
model: "k2p5",
modelProvider: "kimi-coding",
contextTokens: 262_000,
providerOverride: "anthropic",
modelOverride: "claude-opus-4-5",
}),
);
expect(state.provider).toBe("anthropic");
expect(state.model).toBe("claude-opus-4-5");
});
it("uses default provider when providerOverride is not set but modelOverride is", async () => {
const cfg = {} as OpenClawConfig;
const sessionKey = "agent:main:main";
const sessionEntry = makeEntry({
modelOverride: "deepseek-v3-4bit-mlx",
});
const sessionStore = { [sessionKey]: sessionEntry };
const state = await createModelSelectionState({
cfg,
agentCfg: undefined,
sessionEntry,
sessionStore,
sessionKey,
defaultProvider,
defaultModel,
provider: defaultProvider,
model: defaultModel,
hasModelDirective: false,
});
const state = await resolveState(
makeEntry({
modelOverride: "deepseek-v3-4bit-mlx",
}),
);
expect(state.provider).toBe(defaultProvider);
expect(state.model).toBe("deepseek-v3-4bit-mlx");