mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 15:31:07 +00:00
perf(inbound): trim cold startup import graph (#52082)
* perf(inbound): trim cold startup import graph * chore(reply): drop redundant inline action type import * fix(inbound): restore warning and maintenance seams * fix(reply): restore type seam and secure forked transcripts
This commit is contained in:
@@ -1,12 +1,29 @@
|
||||
import { normalizeChatType } from "openclaw/plugin-sdk/account-resolution";
|
||||
import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime";
|
||||
type DiscordSessionKeyContext = {
|
||||
ChatType?: string;
|
||||
From?: string;
|
||||
SenderId?: string;
|
||||
};
|
||||
|
||||
function normalizeDiscordChatType(raw?: string): "direct" | "group" | "channel" | undefined {
|
||||
const normalized = (raw ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (normalized === "dm") {
|
||||
return "direct";
|
||||
}
|
||||
if (normalized === "group" || normalized === "channel" || normalized === "direct") {
|
||||
return normalized;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeExplicitDiscordSessionKey(
|
||||
sessionKey: string,
|
||||
ctx: Pick<MsgContext, "ChatType" | "From" | "SenderId">,
|
||||
ctx: DiscordSessionKeyContext,
|
||||
): string {
|
||||
let normalized = sessionKey.trim().toLowerCase();
|
||||
if (normalizeChatType(ctx.ChatType) !== "direct") {
|
||||
if (normalizeDiscordChatType(ctx.ChatType) !== "direct") {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
|
||||
8
src/agents/context-cache.ts
Normal file
8
src/agents/context-cache.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const MODEL_CONTEXT_TOKEN_CACHE = new Map<string, number>();
|
||||
|
||||
export function lookupCachedContextTokens(modelId?: string): number | undefined {
|
||||
if (!modelId) {
|
||||
return undefined;
|
||||
}
|
||||
return MODEL_CONTEXT_TOKEN_CACHE.get(modelId);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { computeBackoff, type BackoffPolicy } from "../infra/backoff.js";
|
||||
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
|
||||
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
||||
import { lookupCachedContextTokens, MODEL_CONTEXT_TOKEN_CACHE } from "./context-cache.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
|
||||
type ModelEntry = { id: string; contextWindow?: number };
|
||||
@@ -78,7 +79,6 @@ export function applyConfiguredContextWindows(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const MODEL_CACHE = new Map<string, number>();
|
||||
let loadPromise: Promise<void> | null = null;
|
||||
let configuredConfig: OpenClawConfig | undefined;
|
||||
let configLoadFailures = 0;
|
||||
@@ -169,7 +169,7 @@ function primeConfiguredContextWindows(): OpenClawConfig | undefined {
|
||||
try {
|
||||
const cfg = loadConfig();
|
||||
applyConfiguredContextWindows({
|
||||
cache: MODEL_CACHE,
|
||||
cache: MODEL_CONTEXT_TOKEN_CACHE,
|
||||
modelsConfig: cfg.models as ModelsConfig | undefined,
|
||||
});
|
||||
configuredConfig = cfg;
|
||||
@@ -213,7 +213,7 @@ function ensureContextWindowCacheLoaded(): Promise<void> {
|
||||
? modelRegistry.getAvailable()
|
||||
: modelRegistry.getAll();
|
||||
applyDiscoveredContextWindows({
|
||||
cache: MODEL_CACHE,
|
||||
cache: MODEL_CONTEXT_TOKEN_CACHE,
|
||||
models,
|
||||
});
|
||||
} catch {
|
||||
@@ -221,7 +221,7 @@ function ensureContextWindowCacheLoaded(): Promise<void> {
|
||||
}
|
||||
|
||||
applyConfiguredContextWindows({
|
||||
cache: MODEL_CACHE,
|
||||
cache: MODEL_CONTEXT_TOKEN_CACHE,
|
||||
modelsConfig: cfg.models as ModelsConfig | undefined,
|
||||
});
|
||||
})().catch(() => {
|
||||
@@ -241,7 +241,7 @@ export function lookupContextTokens(
|
||||
if (options?.allowAsyncLoad !== false) {
|
||||
void ensureContextWindowCacheLoaded();
|
||||
}
|
||||
return MODEL_CACHE.get(modelId);
|
||||
return lookupCachedContextTokens(modelId);
|
||||
}
|
||||
|
||||
if (shouldEagerWarmContextWindowCache()) {
|
||||
|
||||
1
src/agents/openclaw-tools.runtime.ts
Normal file
1
src/agents/openclaw-tools.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { createOpenClawTools } from "./openclaw-tools.js";
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
listChatCommandsForConfig,
|
||||
normalizeCommandBody,
|
||||
} from "./commands-registry.js";
|
||||
import { isAbortTrigger } from "./reply/abort.js";
|
||||
import { isAbortTrigger } from "./reply/abort-primitives.js";
|
||||
|
||||
export function hasControlCommand(
|
||||
text?: string,
|
||||
|
||||
33
src/auto-reply/reply/abort-cutoff.runtime.ts
Normal file
33
src/auto-reply/reply/abort-cutoff.runtime.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { updateSessionStore } from "../../config/sessions/store.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { applyAbortCutoffToSessionEntry, hasAbortCutoff } from "./abort-cutoff.js";
|
||||
|
||||
export async function clearAbortCutoffInSessionRuntime(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
storePath?: string;
|
||||
}): Promise<boolean> {
|
||||
const { sessionEntry, sessionStore, sessionKey, storePath } = params;
|
||||
if (!sessionEntry || !sessionStore || !sessionKey || !hasAbortCutoff(sessionEntry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
applyAbortCutoffToSessionEntry(sessionEntry, undefined);
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
|
||||
if (storePath) {
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
const existing = store[sessionKey] ?? sessionEntry;
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
applyAbortCutoffToSessionEntry(existing, undefined);
|
||||
existing.updatedAt = Date.now();
|
||||
store[sessionKey] = existing;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { updateSessionStore } from "../../config/sessions.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
|
||||
export type AbortCutoff = {
|
||||
@@ -51,36 +50,6 @@ export function applyAbortCutoffToSessionEntry(
|
||||
entry.abortCutoffTimestamp = cutoff?.timestamp;
|
||||
}
|
||||
|
||||
export async function clearAbortCutoffInSession(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
storePath?: string;
|
||||
}): Promise<boolean> {
|
||||
const { sessionEntry, sessionStore, sessionKey, storePath } = params;
|
||||
if (!sessionEntry || !sessionStore || !sessionKey || !hasAbortCutoff(sessionEntry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
applyAbortCutoffToSessionEntry(sessionEntry, undefined);
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
|
||||
if (storePath) {
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
const existing = store[sessionKey] ?? sessionEntry;
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
applyAbortCutoffToSessionEntry(existing, undefined);
|
||||
existing.updatedAt = Date.now();
|
||||
store[sessionKey] = existing;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function toNumericMessageSid(value: string | undefined): bigint | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed || !/^\d+$/.test(trimmed)) {
|
||||
|
||||
130
src/auto-reply/reply/abort-primitives.ts
Normal file
130
src/auto-reply/reply/abort-primitives.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { normalizeCommandBody, type CommandNormalizeOptions } from "../commands-registry.js";
|
||||
|
||||
const ABORT_TRIGGERS = new Set([
|
||||
"stop",
|
||||
"esc",
|
||||
"abort",
|
||||
"wait",
|
||||
"exit",
|
||||
"interrupt",
|
||||
"detente",
|
||||
"deten",
|
||||
"detén",
|
||||
"arrete",
|
||||
"arrête",
|
||||
"停止",
|
||||
"やめて",
|
||||
"止めて",
|
||||
"रुको",
|
||||
"توقف",
|
||||
"стоп",
|
||||
"остановись",
|
||||
"останови",
|
||||
"остановить",
|
||||
"прекрати",
|
||||
"halt",
|
||||
"anhalten",
|
||||
"aufhören",
|
||||
"hoer auf",
|
||||
"stopp",
|
||||
"pare",
|
||||
"stop openclaw",
|
||||
"openclaw stop",
|
||||
"stop action",
|
||||
"stop current action",
|
||||
"stop run",
|
||||
"stop current run",
|
||||
"stop agent",
|
||||
"stop the agent",
|
||||
"stop don't do anything",
|
||||
"stop dont do anything",
|
||||
"stop do not do anything",
|
||||
"stop doing anything",
|
||||
"do not do that",
|
||||
"please stop",
|
||||
"stop please",
|
||||
]);
|
||||
const ABORT_MEMORY = new Map<string, boolean>();
|
||||
const ABORT_MEMORY_MAX = 2000;
|
||||
const TRAILING_ABORT_PUNCTUATION_RE = /[.!?…,,。;;::'"’”)\]}]+$/u;
|
||||
|
||||
function normalizeAbortTriggerText(text: string): string {
|
||||
return text
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[’`]/g, "'")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(TRAILING_ABORT_PUNCTUATION_RE, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function isAbortTrigger(text?: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeAbortTriggerText(text);
|
||||
return ABORT_TRIGGERS.has(normalized);
|
||||
}
|
||||
|
||||
export function isAbortRequestText(text?: string, options?: CommandNormalizeOptions): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeCommandBody(text, options).trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const normalizedLower = normalized.toLowerCase();
|
||||
return (
|
||||
normalizedLower === "/stop" ||
|
||||
normalizeAbortTriggerText(normalizedLower) === "/stop" ||
|
||||
isAbortTrigger(normalizedLower)
|
||||
);
|
||||
}
|
||||
|
||||
export function getAbortMemory(key: string): boolean | undefined {
|
||||
const normalized = key.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return ABORT_MEMORY.get(normalized);
|
||||
}
|
||||
|
||||
function pruneAbortMemory(): void {
|
||||
if (ABORT_MEMORY.size <= ABORT_MEMORY_MAX) {
|
||||
return;
|
||||
}
|
||||
const excess = ABORT_MEMORY.size - ABORT_MEMORY_MAX;
|
||||
let removed = 0;
|
||||
for (const entryKey of ABORT_MEMORY.keys()) {
|
||||
ABORT_MEMORY.delete(entryKey);
|
||||
removed += 1;
|
||||
if (removed >= excess) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setAbortMemory(key: string, value: boolean): void {
|
||||
const normalized = key.trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
ABORT_MEMORY.delete(normalized);
|
||||
return;
|
||||
}
|
||||
if (ABORT_MEMORY.has(normalized)) {
|
||||
ABORT_MEMORY.delete(normalized);
|
||||
}
|
||||
ABORT_MEMORY.set(normalized, true);
|
||||
pruneAbortMemory();
|
||||
}
|
||||
|
||||
export function getAbortMemorySizeForTest(): number {
|
||||
return ABORT_MEMORY.size;
|
||||
}
|
||||
|
||||
export function resetAbortMemoryForTest(): void {
|
||||
ABORT_MEMORY.clear();
|
||||
}
|
||||
@@ -20,147 +20,32 @@ import {
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import { normalizeCommandBody, type CommandNormalizeOptions } from "../commands-registry.js";
|
||||
import type { FinalizedMsgContext, MsgContext } from "../templating.js";
|
||||
import {
|
||||
applyAbortCutoffToSessionEntry,
|
||||
resolveAbortCutoffFromContext,
|
||||
shouldPersistAbortCutoff,
|
||||
} from "./abort-cutoff.js";
|
||||
import {
|
||||
getAbortMemory,
|
||||
getAbortMemorySizeForTest,
|
||||
isAbortRequestText,
|
||||
isAbortTrigger,
|
||||
resetAbortMemoryForTest,
|
||||
setAbortMemory,
|
||||
} from "./abort-primitives.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
import { clearSessionQueues } from "./queue.js";
|
||||
|
||||
export { resolveAbortCutoffFromContext, shouldSkipMessageByAbortCutoff } from "./abort-cutoff.js";
|
||||
|
||||
const ABORT_TRIGGERS = new Set([
|
||||
"stop",
|
||||
"esc",
|
||||
"abort",
|
||||
"wait",
|
||||
"exit",
|
||||
"interrupt",
|
||||
"detente",
|
||||
"deten",
|
||||
"detén",
|
||||
"arrete",
|
||||
"arrête",
|
||||
"停止",
|
||||
"やめて",
|
||||
"止めて",
|
||||
"रुको",
|
||||
"توقف",
|
||||
"стоп",
|
||||
"остановись",
|
||||
"останови",
|
||||
"остановить",
|
||||
"прекрати",
|
||||
"halt",
|
||||
"anhalten",
|
||||
"aufhören",
|
||||
"hoer auf",
|
||||
"stopp",
|
||||
"pare",
|
||||
"stop openclaw",
|
||||
"openclaw stop",
|
||||
"stop action",
|
||||
"stop current action",
|
||||
"stop run",
|
||||
"stop current run",
|
||||
"stop agent",
|
||||
"stop the agent",
|
||||
"stop don't do anything",
|
||||
"stop dont do anything",
|
||||
"stop do not do anything",
|
||||
"stop doing anything",
|
||||
"do not do that",
|
||||
"please stop",
|
||||
"stop please",
|
||||
]);
|
||||
const ABORT_MEMORY = new Map<string, boolean>();
|
||||
const ABORT_MEMORY_MAX = 2000;
|
||||
const TRAILING_ABORT_PUNCTUATION_RE = /[.!?…,,。;;::'"’”)\]}]+$/u;
|
||||
|
||||
function normalizeAbortTriggerText(text: string): string {
|
||||
return text
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[’`]/g, "'")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(TRAILING_ABORT_PUNCTUATION_RE, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function isAbortTrigger(text?: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeAbortTriggerText(text);
|
||||
return ABORT_TRIGGERS.has(normalized);
|
||||
}
|
||||
|
||||
export function isAbortRequestText(text?: string, options?: CommandNormalizeOptions): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeCommandBody(text, options).trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const normalizedLower = normalized.toLowerCase();
|
||||
return (
|
||||
normalizedLower === "/stop" ||
|
||||
normalizeAbortTriggerText(normalizedLower) === "/stop" ||
|
||||
isAbortTrigger(normalizedLower)
|
||||
);
|
||||
}
|
||||
|
||||
export function getAbortMemory(key: string): boolean | undefined {
|
||||
const normalized = key.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return ABORT_MEMORY.get(normalized);
|
||||
}
|
||||
|
||||
function pruneAbortMemory(): void {
|
||||
if (ABORT_MEMORY.size <= ABORT_MEMORY_MAX) {
|
||||
return;
|
||||
}
|
||||
const excess = ABORT_MEMORY.size - ABORT_MEMORY_MAX;
|
||||
let removed = 0;
|
||||
for (const entryKey of ABORT_MEMORY.keys()) {
|
||||
ABORT_MEMORY.delete(entryKey);
|
||||
removed += 1;
|
||||
if (removed >= excess) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setAbortMemory(key: string, value: boolean): void {
|
||||
const normalized = key.trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
ABORT_MEMORY.delete(normalized);
|
||||
return;
|
||||
}
|
||||
// Refresh insertion order so active keys are less likely to be evicted.
|
||||
if (ABORT_MEMORY.has(normalized)) {
|
||||
ABORT_MEMORY.delete(normalized);
|
||||
}
|
||||
ABORT_MEMORY.set(normalized, true);
|
||||
pruneAbortMemory();
|
||||
}
|
||||
|
||||
export function getAbortMemorySizeForTest(): number {
|
||||
return ABORT_MEMORY.size;
|
||||
}
|
||||
|
||||
export function resetAbortMemoryForTest(): void {
|
||||
ABORT_MEMORY.clear();
|
||||
}
|
||||
export {
|
||||
getAbortMemory,
|
||||
getAbortMemorySizeForTest,
|
||||
isAbortRequestText,
|
||||
isAbortTrigger,
|
||||
resetAbortMemoryForTest,
|
||||
setAbortMemory,
|
||||
};
|
||||
|
||||
export function formatAbortReplyText(stoppedSubagents?: number): string {
|
||||
if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { updateSessionStore } from "../../config/sessions.js";
|
||||
import { setAbortMemory } from "./abort.js";
|
||||
import { setAbortMemory } from "./abort-primitives.js";
|
||||
|
||||
export async function applySessionHints(params: {
|
||||
baseBody: string;
|
||||
|
||||
1
src/auto-reply/reply/commands-core.runtime.ts
Normal file
1
src/auto-reply/reply/commands-core.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { emitResetCommandHooks } from "./commands-core.js";
|
||||
1
src/auto-reply/reply/commands.runtime.ts
Normal file
1
src/auto-reply/reply/commands.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { buildStatusReply, handleCommands } from "./commands.js";
|
||||
27
src/auto-reply/reply/directive-handling.auth-profile.ts
Normal file
27
src/auto-reply/reply/directive-handling.auth-profile.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ensureAuthProfileStore } from "../../agents/auth-profiles.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
export function resolveProfileOverride(params: {
|
||||
rawProfile?: string;
|
||||
provider: string;
|
||||
cfg: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
}): { profileId?: string; error?: string } {
|
||||
const raw = params.rawProfile?.trim();
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profile = store.profiles[raw];
|
||||
if (!profile) {
|
||||
return { error: `Auth profile "${raw}" not found.` };
|
||||
}
|
||||
if (profile.provider !== params.provider) {
|
||||
return {
|
||||
error: `Auth profile "${raw}" is for ${profile.provider}, not ${params.provider}.`,
|
||||
};
|
||||
}
|
||||
return { profileId: raw };
|
||||
}
|
||||
@@ -217,27 +217,4 @@ export const formatAuthLabel = (auth: { label: string; source: string }) => {
|
||||
return `${auth.label} (${auth.source})`;
|
||||
};
|
||||
|
||||
export const resolveProfileOverride = (params: {
|
||||
rawProfile?: string;
|
||||
provider: string;
|
||||
cfg: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
}): { profileId?: string; error?: string } => {
|
||||
const raw = params.rawProfile?.trim();
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profile = store.profiles[raw];
|
||||
if (!profile) {
|
||||
return { error: `Auth profile "${raw}" not found.` };
|
||||
}
|
||||
if (profile.provider !== params.provider) {
|
||||
return {
|
||||
error: `Auth profile "${raw}" is for ${profile.provider}, not ${params.provider}.`,
|
||||
};
|
||||
}
|
||||
return { profileId: raw };
|
||||
};
|
||||
export { resolveProfileOverride } from "./directive-handling.auth-profile.js";
|
||||
|
||||
24
src/auto-reply/reply/directive-handling.defaults.ts
Normal file
24
src/auto-reply/reply/directive-handling.defaults.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
type ModelAliasIndex,
|
||||
resolveDefaultModelForAgent,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
export function resolveDefaultModel(params: { cfg: OpenClawConfig; agentId?: string }): {
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
} {
|
||||
const mainModel = resolveDefaultModelForAgent({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const defaultProvider = mainModel.provider;
|
||||
const defaultModel = mainModel.model;
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: params.cfg,
|
||||
defaultProvider,
|
||||
});
|
||||
return { defaultProvider, defaultModel, aliasIndex };
|
||||
}
|
||||
@@ -13,10 +13,8 @@ import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import {
|
||||
maybeHandleModelDirectiveInfo,
|
||||
resolveModelSelectionFromDirective,
|
||||
} from "./directive-handling.model.js";
|
||||
import { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js";
|
||||
import { maybeHandleModelDirectiveInfo } from "./directive-handling.model.js";
|
||||
import type { HandleDirectiveOnlyParams } from "./directive-handling.params.js";
|
||||
import { maybeHandleQueueDirective } from "./directive-handling.queue-validation.js";
|
||||
import {
|
||||
|
||||
159
src/auto-reply/reply/directive-handling.model-selection.ts
Normal file
159
src/auto-reply/reply/directive-handling.model-selection.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { ensureAuthProfileStore } from "../../agents/auth-profiles.js";
|
||||
import {
|
||||
type ModelAliasIndex,
|
||||
modelKey,
|
||||
normalizeProviderIdForAuth,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveProfileOverride } from "./directive-handling.auth-profile.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import { type ModelDirectiveSelection, resolveModelDirectiveSelection } from "./model-selection.js";
|
||||
|
||||
function resolveStoredNumericProfileModelDirective(params: { raw: string; agentDir: string }): {
|
||||
modelRaw: string;
|
||||
profileId: string;
|
||||
profileProvider: string;
|
||||
} | null {
|
||||
const trimmed = params.raw.trim();
|
||||
const lastSlash = trimmed.lastIndexOf("/");
|
||||
const profileDelimiter = trimmed.indexOf("@", lastSlash + 1);
|
||||
if (profileDelimiter <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileId = trimmed.slice(profileDelimiter + 1).trim();
|
||||
if (!/^\d{8}$/.test(profileId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const modelRaw = trimmed.slice(0, profileDelimiter).trim();
|
||||
if (!modelRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profile = store.profiles[profileId];
|
||||
if (!profile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { modelRaw, profileId, profileProvider: profile.provider };
|
||||
}
|
||||
|
||||
export function resolveModelSelectionFromDirective(params: {
|
||||
directives: InlineDirectives;
|
||||
cfg: OpenClawConfig;
|
||||
agentDir: string;
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
allowedModelKeys: Set<string>;
|
||||
allowedModelCatalog: Array<{ provider: string; id?: string; name?: string }>;
|
||||
provider: string;
|
||||
}): {
|
||||
modelSelection?: ModelDirectiveSelection;
|
||||
profileOverride?: string;
|
||||
errorText?: string;
|
||||
} {
|
||||
if (!params.directives.hasModelDirective || !params.directives.rawModelDirective) {
|
||||
if (params.directives.rawModelProfile) {
|
||||
return { errorText: "Auth profile override requires a model selection." };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const raw = params.directives.rawModelDirective.trim();
|
||||
const storedNumericProfile =
|
||||
params.directives.rawModelProfile === undefined
|
||||
? resolveStoredNumericProfileModelDirective({
|
||||
raw,
|
||||
agentDir: params.agentDir,
|
||||
})
|
||||
: null;
|
||||
const storedNumericProfileSelection = storedNumericProfile
|
||||
? resolveModelDirectiveSelection({
|
||||
raw: storedNumericProfile.modelRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
aliasIndex: params.aliasIndex,
|
||||
allowedModelKeys: params.allowedModelKeys,
|
||||
})
|
||||
: null;
|
||||
const useStoredNumericProfile =
|
||||
Boolean(storedNumericProfileSelection?.selection) &&
|
||||
normalizeProviderIdForAuth(storedNumericProfileSelection?.selection?.provider ?? "") ===
|
||||
normalizeProviderIdForAuth(storedNumericProfile?.profileProvider ?? "");
|
||||
const modelRaw =
|
||||
useStoredNumericProfile && storedNumericProfile ? storedNumericProfile.modelRaw : raw;
|
||||
let modelSelection: ModelDirectiveSelection | undefined;
|
||||
|
||||
if (/^[0-9]+$/.test(raw)) {
|
||||
return {
|
||||
errorText: [
|
||||
"Numeric model selection is not supported in chat.",
|
||||
"",
|
||||
"Browse: /models or /models <provider>",
|
||||
"Switch: /model <provider/model>",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
const explicit = resolveModelRefFromString({
|
||||
raw: modelRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex: params.aliasIndex,
|
||||
});
|
||||
if (explicit) {
|
||||
const explicitKey = modelKey(explicit.ref.provider, explicit.ref.model);
|
||||
if (params.allowedModelKeys.size === 0 || params.allowedModelKeys.has(explicitKey)) {
|
||||
modelSelection = {
|
||||
provider: explicit.ref.provider,
|
||||
model: explicit.ref.model,
|
||||
isDefault:
|
||||
explicit.ref.provider === params.defaultProvider &&
|
||||
explicit.ref.model === params.defaultModel,
|
||||
...(explicit.alias ? { alias: explicit.alias } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!modelSelection) {
|
||||
const resolved = resolveModelDirectiveSelection({
|
||||
raw: modelRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
aliasIndex: params.aliasIndex,
|
||||
allowedModelKeys: params.allowedModelKeys,
|
||||
});
|
||||
|
||||
if (resolved.error) {
|
||||
return { errorText: resolved.error };
|
||||
}
|
||||
|
||||
if (resolved.selection) {
|
||||
modelSelection = resolved.selection;
|
||||
}
|
||||
}
|
||||
|
||||
let profileOverride: string | undefined;
|
||||
const rawProfile =
|
||||
params.directives.rawModelProfile ??
|
||||
(useStoredNumericProfile ? storedNumericProfile?.profileId : undefined);
|
||||
if (modelSelection && rawProfile) {
|
||||
const profileResolved = resolveProfileOverride({
|
||||
rawProfile,
|
||||
provider: modelSelection.provider,
|
||||
cfg: params.cfg,
|
||||
agentDir: params.agentDir,
|
||||
});
|
||||
if (profileResolved.error) {
|
||||
return { errorText: profileResolved.error };
|
||||
}
|
||||
profileOverride = profileResolved.profileId;
|
||||
}
|
||||
|
||||
return { modelSelection, profileOverride };
|
||||
}
|
||||
@@ -3,20 +3,16 @@ import {
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
import { lookupCachedContextTokens } from "../../agents/context-cache.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
type ModelAliasIndex,
|
||||
resolveDefaultModelForAgent,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { updateSessionStore } from "../../config/sessions/store.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
import { resolveModelSelectionFromDirective } from "./directive-handling.model.js";
|
||||
import { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import { enqueueModeSwitchEvents } from "./directive-handling.shared.js";
|
||||
import type { ElevatedLevel, ReasoningLevel } from "./directives.js";
|
||||
@@ -203,24 +199,7 @@ export async function persistInlineDirectives(params: {
|
||||
return {
|
||||
provider,
|
||||
model,
|
||||
contextTokens: agentCfg?.contextTokens ?? lookupContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS,
|
||||
contextTokens:
|
||||
agentCfg?.contextTokens ?? lookupCachedContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDefaultModel(params: { cfg: OpenClawConfig; agentId?: string }): {
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
} {
|
||||
const mainModel = resolveDefaultModelForAgent({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const defaultProvider = mainModel.provider;
|
||||
const defaultModel = mainModel.model;
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: params.cfg,
|
||||
defaultProvider,
|
||||
});
|
||||
return { defaultProvider, defaultModel, aliasIndex };
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@ export { applyInlineDirectivesFastLane } from "./directive-handling.fast-lane.js
|
||||
export * from "./directive-handling.impl.js";
|
||||
export type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
export { isDirectiveOnly, parseInlineDirectives } from "./directive-handling.parse.js";
|
||||
export { persistInlineDirectives, resolveDefaultModel } from "./directive-handling.persist.js";
|
||||
export { persistInlineDirectives } from "./directive-handling.persist.js";
|
||||
export { resolveDefaultModel } from "./directive-handling.defaults.js";
|
||||
export { formatDirectiveAck } from "./directive-handling.shared.js";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { collectTextContentBlocks } from "../../agents/content-blocks.js";
|
||||
import { createOpenClawTools } from "../../agents/openclaw-tools.js";
|
||||
import type { BlockReplyChunking } from "../../agents/pi-embedded-block-chunker.js";
|
||||
import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||
import { applyOwnerOnlyToolPolicy } from "../../agents/tool-policy.js";
|
||||
@@ -11,20 +10,18 @@ import { generateSecureToken } from "../../infra/secure-random.js";
|
||||
import { resolveGatewayMessageChannel } from "../../utils/message-channel.js";
|
||||
import {
|
||||
listReservedChatSlashCommandNames,
|
||||
listSkillCommandsForWorkspace,
|
||||
resolveSkillCommandInvocation,
|
||||
} from "../skill-commands.js";
|
||||
} from "../skill-commands-base.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import {
|
||||
clearAbortCutoffInSession,
|
||||
readAbortCutoffFromSessionEntry,
|
||||
resolveAbortCutoffFromContext,
|
||||
shouldSkipMessageByAbortCutoff,
|
||||
} from "./abort-cutoff.js";
|
||||
import { getAbortMemory, isAbortRequestText } from "./abort.js";
|
||||
import { buildStatusReply, handleCommands } from "./commands.js";
|
||||
import { getAbortMemory, isAbortRequestText } from "./abort-primitives.js";
|
||||
import type { buildStatusReply, handleCommands } from "./commands.runtime.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import { isDirectiveOnly } from "./directive-handling.parse.js";
|
||||
import type { createModelSelectionState } from "./model-selection.js";
|
||||
@@ -191,7 +188,7 @@ export async function handleInlineActions(params: {
|
||||
shouldLoadSkillCommands && params.skillCommands
|
||||
? params.skillCommands
|
||||
: shouldLoadSkillCommands
|
||||
? listSkillCommandsForWorkspace({
|
||||
? (await import("../skill-commands.runtime.js")).listSkillCommandsForWorkspace({
|
||||
workspaceDir,
|
||||
cfg,
|
||||
skillFilter,
|
||||
@@ -222,6 +219,7 @@ export async function handleInlineActions(params: {
|
||||
resolveGatewayMessageChannel(ctx.Provider) ??
|
||||
undefined;
|
||||
|
||||
const { createOpenClawTools } = await import("../../agents/openclaw-tools.runtime.js");
|
||||
const tools = createOpenClawTools({
|
||||
agentSessionKey: sessionKey,
|
||||
agentChannel: channel,
|
||||
@@ -305,7 +303,9 @@ export async function handleInlineActions(params: {
|
||||
return { kind: "reply", reply: undefined };
|
||||
}
|
||||
if (cutoff) {
|
||||
await clearAbortCutoffInSession({
|
||||
await (
|
||||
await import("./abort-cutoff.runtime.js")
|
||||
).clearAbortCutoffInSessionRuntime({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
@@ -335,6 +335,7 @@ export async function handleInlineActions(params: {
|
||||
isGroup,
|
||||
}) && inlineStatusRequested;
|
||||
if (handleInlineStatus) {
|
||||
const { buildStatusReply } = await import("./commands.runtime.js");
|
||||
const inlineStatusReply = await buildStatusReply({
|
||||
cfg,
|
||||
command,
|
||||
@@ -358,8 +359,9 @@ export async function handleInlineActions(params: {
|
||||
directives = { ...directives, hasStatusDirective: false };
|
||||
}
|
||||
|
||||
const runCommands = (commandInput: typeof command) =>
|
||||
handleCommands({
|
||||
const runCommands = async (commandInput: typeof command) => {
|
||||
const { handleCommands } = await import("./commands.runtime.js");
|
||||
return handleCommands({
|
||||
// Pass sessionCtx so command handlers can mutate stripped body for same-turn continuation.
|
||||
ctx: sessionCtx,
|
||||
// Keep original finalized context in sync when command handlers need outer-dispatch side effects.
|
||||
@@ -397,6 +399,7 @@ export async function handleInlineActions(params: {
|
||||
skillCommands,
|
||||
typing,
|
||||
});
|
||||
};
|
||||
|
||||
if (inlineCommand) {
|
||||
const inlineCommandContext = {
|
||||
|
||||
@@ -15,8 +15,7 @@ import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import { emitResetCommandHooks, type ResetCommandAction } from "./commands-core.js";
|
||||
import { resolveDefaultModel } from "./directive-handling.persist.js";
|
||||
import { resolveDefaultModel } from "./directive-handling.defaults.js";
|
||||
import { resolveReplyDirectives } from "./get-reply-directives.js";
|
||||
import { handleInlineActions } from "./get-reply-inline-actions.js";
|
||||
import { runPreparedReply } from "./get-reply-run.js";
|
||||
@@ -31,6 +30,8 @@ function shouldLogCoreIngressTiming(): boolean {
|
||||
return process.env.OPENCLAW_DEBUG_INGRESS_TIMING === "1";
|
||||
}
|
||||
|
||||
type ResetCommandAction = "new" | "reset";
|
||||
|
||||
function mergeSkillFilters(channelFilter?: string[], agentFilter?: string[]): string[] | undefined {
|
||||
const normalize = (list?: string[]) => {
|
||||
if (!Array.isArray(list)) {
|
||||
@@ -355,6 +356,7 @@ export async function getReplyFromConfig(
|
||||
if (!resetMatch) {
|
||||
return;
|
||||
}
|
||||
const { emitResetCommandHooks } = await import("./commands-core.runtime.js");
|
||||
const action: ResetCommandAction = resetMatch[1] === "reset" ? "reset" : "new";
|
||||
await emitResetCommandHooks({
|
||||
action,
|
||||
|
||||
52
src/auto-reply/reply/session-fork.runtime.ts
Normal file
52
src/auto-reply/reply/session-fork.runtime.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { resolveSessionFilePath } from "../../config/sessions/paths.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
|
||||
export function forkSessionFromParentRuntime(params: {
|
||||
parentEntry: SessionEntry;
|
||||
agentId: string;
|
||||
sessionsDir: string;
|
||||
}): { sessionId: string; sessionFile: string } | null {
|
||||
const parentSessionFile = resolveSessionFilePath(
|
||||
params.parentEntry.sessionId,
|
||||
params.parentEntry,
|
||||
{ agentId: params.agentId, sessionsDir: params.sessionsDir },
|
||||
);
|
||||
if (!parentSessionFile || !fs.existsSync(parentSessionFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const manager = SessionManager.open(parentSessionFile);
|
||||
const leafId = manager.getLeafId();
|
||||
if (leafId) {
|
||||
const sessionFile = manager.createBranchedSession(leafId) ?? manager.getSessionFile();
|
||||
const sessionId = manager.getSessionId();
|
||||
if (sessionFile && sessionId) {
|
||||
return { sessionId, sessionFile };
|
||||
}
|
||||
}
|
||||
const sessionId = crypto.randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
||||
const sessionFile = path.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
|
||||
const header = {
|
||||
type: "session",
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: sessionId,
|
||||
timestamp,
|
||||
cwd: manager.getCwd(),
|
||||
parentSession: parentSessionFile,
|
||||
};
|
||||
fs.writeFileSync(sessionFile, `${JSON.stringify(header)}\n`, {
|
||||
encoding: "utf-8",
|
||||
mode: 0o600,
|
||||
flag: "wx",
|
||||
});
|
||||
return { sessionId, sessionFile };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveSessionFilePath } from "../../config/sessions/paths.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
|
||||
/**
|
||||
@@ -21,44 +16,11 @@ export function resolveParentForkMaxTokens(cfg: OpenClawConfig): number {
|
||||
return DEFAULT_PARENT_FORK_MAX_TOKENS;
|
||||
}
|
||||
|
||||
export function forkSessionFromParent(params: {
|
||||
export async function forkSessionFromParent(params: {
|
||||
parentEntry: SessionEntry;
|
||||
agentId: string;
|
||||
sessionsDir: string;
|
||||
}): { sessionId: string; sessionFile: string } | null {
|
||||
const parentSessionFile = resolveSessionFilePath(
|
||||
params.parentEntry.sessionId,
|
||||
params.parentEntry,
|
||||
{ agentId: params.agentId, sessionsDir: params.sessionsDir },
|
||||
);
|
||||
if (!parentSessionFile || !fs.existsSync(parentSessionFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const manager = SessionManager.open(parentSessionFile);
|
||||
const leafId = manager.getLeafId();
|
||||
if (leafId) {
|
||||
const sessionFile = manager.createBranchedSession(leafId) ?? manager.getSessionFile();
|
||||
const sessionId = manager.getSessionId();
|
||||
if (sessionFile && sessionId) {
|
||||
return { sessionId, sessionFile };
|
||||
}
|
||||
}
|
||||
const sessionId = crypto.randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
||||
const sessionFile = path.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
|
||||
const header = {
|
||||
type: "session",
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: sessionId,
|
||||
timestamp,
|
||||
cwd: manager.getCwd(),
|
||||
parentSession: parentSessionFile,
|
||||
};
|
||||
fs.writeFileSync(sessionFile, `${JSON.stringify(header)}\n`, "utf-8");
|
||||
return { sessionId, sessionFile };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}): Promise<{ sessionId: string; sessionFile: string } | null> {
|
||||
const runtime = await import("./session-fork.runtime.js");
|
||||
return runtime.forkSessionFromParentRuntime(params);
|
||||
}
|
||||
|
||||
@@ -501,7 +501,7 @@ export async function initSessionState(params: {
|
||||
`forking from parent session: parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
|
||||
`parentTokens=${parentTokens}`,
|
||||
);
|
||||
const forked = forkSessionFromParent({
|
||||
const forked = await forkSessionFromParent({
|
||||
parentEntry: sessionStore[parentSessionKey],
|
||||
agentId,
|
||||
sessionsDir: path.dirname(storePath),
|
||||
|
||||
96
src/auto-reply/skill-commands-base.ts
Normal file
96
src/auto-reply/skill-commands-base.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||
import { getChatCommands } from "./commands-registry.data.js";
|
||||
|
||||
export function listReservedChatSlashCommandNames(extraNames: string[] = []): Set<string> {
|
||||
const reserved = new Set<string>();
|
||||
for (const command of getChatCommands()) {
|
||||
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 extraNames) {
|
||||
const trimmed = name.trim().toLowerCase();
|
||||
if (trimmed) {
|
||||
reserved.add(trimmed);
|
||||
}
|
||||
}
|
||||
return reserved;
|
||||
}
|
||||
|
||||
function normalizeSkillCommandLookup(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_]+/g, "-");
|
||||
}
|
||||
|
||||
function findSkillCommand(
|
||||
skillCommands: SkillCommandSpec[],
|
||||
rawName: string,
|
||||
): SkillCommandSpec | undefined {
|
||||
const trimmed = rawName.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const lowered = trimmed.toLowerCase();
|
||||
const normalized = normalizeSkillCommandLookup(trimmed);
|
||||
return skillCommands.find((entry) => {
|
||||
if (entry.name.toLowerCase() === lowered) {
|
||||
return true;
|
||||
}
|
||||
if (entry.skillName.toLowerCase() === lowered) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
normalizeSkillCommandLookup(entry.name) === normalized ||
|
||||
normalizeSkillCommandLookup(entry.skillName) === normalized
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveSkillCommandInvocation(params: {
|
||||
commandBodyNormalized: string;
|
||||
skillCommands: SkillCommandSpec[];
|
||||
}): { command: SkillCommandSpec; args?: string } | null {
|
||||
const trimmed = params.commandBodyNormalized.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
const match = trimmed.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const commandName = match[1]?.trim().toLowerCase();
|
||||
if (!commandName) {
|
||||
return null;
|
||||
}
|
||||
if (commandName === "skill") {
|
||||
const remainder = match[2]?.trim();
|
||||
if (!remainder) {
|
||||
return null;
|
||||
}
|
||||
const skillMatch = remainder.match(/^([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||
if (!skillMatch) {
|
||||
return null;
|
||||
}
|
||||
const skillCommand = findSkillCommand(params.skillCommands, skillMatch[1] ?? "");
|
||||
if (!skillCommand) {
|
||||
return null;
|
||||
}
|
||||
const args = skillMatch[2]?.trim();
|
||||
return { command: skillCommand, args: args || undefined };
|
||||
}
|
||||
const command = params.skillCommands.find((entry) => entry.name.toLowerCase() === commandName);
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
const args = match[2]?.trim();
|
||||
return { command, args: args || undefined };
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
export {
|
||||
listReservedChatSlashCommandNames,
|
||||
listSkillCommandsForWorkspace,
|
||||
resolveSkillCommandInvocation,
|
||||
} from "./skill-commands.js";
|
||||
export { listSkillCommandsForAgents, listSkillCommandsForWorkspace } from "./skill-commands.js";
|
||||
|
||||
@@ -8,30 +8,11 @@ import { buildWorkspaceSkillCommandSpecs, type SkillCommandSpec } from "../agent
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
||||
import { listChatCommands } from "./commands-registry.js";
|
||||
|
||||
export function listReservedChatSlashCommandNames(extraNames: string[] = []): Set<string> {
|
||||
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 extraNames) {
|
||||
const trimmed = name.trim().toLowerCase();
|
||||
if (trimmed) {
|
||||
reserved.add(trimmed);
|
||||
}
|
||||
}
|
||||
return reserved;
|
||||
}
|
||||
import { listReservedChatSlashCommandNames } from "./skill-commands-base.js";
|
||||
export {
|
||||
listReservedChatSlashCommandNames,
|
||||
resolveSkillCommandInvocation,
|
||||
} from "./skill-commands-base.js";
|
||||
|
||||
export function listSkillCommandsForWorkspace(params: {
|
||||
workspaceDir: string;
|
||||
@@ -131,74 +112,3 @@ export function listSkillCommandsForAgents(params: {
|
||||
export const __testing = {
|
||||
dedupeBySkillName,
|
||||
};
|
||||
|
||||
function normalizeSkillCommandLookup(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_]+/g, "-");
|
||||
}
|
||||
|
||||
function findSkillCommand(
|
||||
skillCommands: SkillCommandSpec[],
|
||||
rawName: string,
|
||||
): SkillCommandSpec | undefined {
|
||||
const trimmed = rawName.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const lowered = trimmed.toLowerCase();
|
||||
const normalized = normalizeSkillCommandLookup(trimmed);
|
||||
return skillCommands.find((entry) => {
|
||||
if (entry.name.toLowerCase() === lowered) {
|
||||
return true;
|
||||
}
|
||||
if (entry.skillName.toLowerCase() === lowered) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
normalizeSkillCommandLookup(entry.name) === normalized ||
|
||||
normalizeSkillCommandLookup(entry.skillName) === normalized
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveSkillCommandInvocation(params: {
|
||||
commandBodyNormalized: string;
|
||||
skillCommands: SkillCommandSpec[];
|
||||
}): { command: SkillCommandSpec; args?: string } | null {
|
||||
const trimmed = params.commandBodyNormalized.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
const match = trimmed.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const commandName = match[1]?.trim().toLowerCase();
|
||||
if (!commandName) {
|
||||
return null;
|
||||
}
|
||||
if (commandName === "skill") {
|
||||
const remainder = match[2]?.trim();
|
||||
if (!remainder) {
|
||||
return null;
|
||||
}
|
||||
const skillMatch = remainder.match(/^([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||
if (!skillMatch) {
|
||||
return null;
|
||||
}
|
||||
const skillCommand = findSkillCommand(params.skillCommands, skillMatch[1] ?? "");
|
||||
if (!skillCommand) {
|
||||
return null;
|
||||
}
|
||||
const args = skillMatch[2]?.trim();
|
||||
return { command: skillCommand, args: args || undefined };
|
||||
}
|
||||
const command = params.skillCommands.find((entry) => entry.name.toLowerCase() === commandName);
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
const args = match[2]?.trim();
|
||||
return { command, args: args || undefined };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isAbortRequestText } from "../auto-reply/reply/abort.js";
|
||||
import { isAbortRequestText } from "../auto-reply/reply/abort-primitives.js";
|
||||
|
||||
export type ChatAbortControllerEntry = {
|
||||
controller: AbortController;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
resolveSessionAgentId: vi.fn(() => "agent-from-key"),
|
||||
resolveSessionDeliveryTarget: vi.fn(() => ({
|
||||
deliveryContextFromSession: vi.fn(() => ({
|
||||
channel: "whatsapp",
|
||||
to: "+15550001",
|
||||
accountId: "acct-1",
|
||||
@@ -50,7 +50,7 @@ describe("deliverSessionMaintenanceWarning", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
vi.resetModules();
|
||||
mocks.resolveSessionAgentId.mockClear();
|
||||
mocks.resolveSessionDeliveryTarget.mockClear();
|
||||
mocks.deliveryContextFromSession.mockClear();
|
||||
mocks.normalizeMessageChannel.mockClear();
|
||||
mocks.isDeliverableMessageChannel.mockClear();
|
||||
mocks.deliverOutboundPayloads.mockClear();
|
||||
@@ -62,10 +62,10 @@ describe("deliverSessionMaintenanceWarning", () => {
|
||||
normalizeMessageChannel: mocks.normalizeMessageChannel,
|
||||
isDeliverableMessageChannel: mocks.isDeliverableMessageChannel,
|
||||
}));
|
||||
vi.doMock("./outbound/targets.js", () => ({
|
||||
resolveSessionDeliveryTarget: mocks.resolveSessionDeliveryTarget,
|
||||
vi.doMock("../utils/delivery-context.js", () => ({
|
||||
deliveryContextFromSession: mocks.deliveryContextFromSession,
|
||||
}));
|
||||
vi.doMock("./outbound/deliver.js", () => ({
|
||||
vi.doMock("./outbound/deliver-runtime.js", () => ({
|
||||
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
|
||||
}));
|
||||
vi.doMock("./system-events.js", () => ({
|
||||
@@ -112,7 +112,7 @@ describe("deliverSessionMaintenanceWarning", () => {
|
||||
});
|
||||
|
||||
it("falls back to a system event when the last target is not deliverable", async () => {
|
||||
mocks.resolveSessionDeliveryTarget.mockReturnValueOnce({
|
||||
mocks.deliveryContextFromSession.mockReturnValueOnce({
|
||||
channel: "debug",
|
||||
to: "+15550001",
|
||||
accountId: "acct-1",
|
||||
@@ -143,7 +143,7 @@ describe("deliverSessionMaintenanceWarning", () => {
|
||||
|
||||
await deliverSessionMaintenanceWarning(createParams());
|
||||
|
||||
expect(mocks.resolveSessionDeliveryTarget).not.toHaveBeenCalled();
|
||||
expect(mocks.deliveryContextFromSession).not.toHaveBeenCalled();
|
||||
expect(mocks.deliverOutboundPayloads).not.toHaveBeenCalled();
|
||||
expect(mocks.enqueueSystemEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SessionMaintenanceWarning } from "../config/sessions/store-maintenance.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { deliveryContextFromSession } from "../utils/delivery-context.js";
|
||||
import { isDeliverableMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js";
|
||||
import { buildOutboundSessionContext } from "./outbound/session-context.js";
|
||||
import { resolveSessionDeliveryTarget } from "./outbound/targets.js";
|
||||
import { enqueueSystemEvent } from "./system-events.js";
|
||||
|
||||
type WarningParams = {
|
||||
@@ -73,6 +73,24 @@ function buildWarningText(warning: SessionMaintenanceWarning): string {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveWarningDeliveryTarget(entry: SessionEntry): {
|
||||
channel?: string;
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
threadId?: string | number;
|
||||
} {
|
||||
const context = deliveryContextFromSession(entry);
|
||||
const channel = context?.channel
|
||||
? (normalizeMessageChannel(context.channel) ?? context.channel)
|
||||
: undefined;
|
||||
return {
|
||||
channel: channel && isDeliverableMessageChannel(channel) ? channel : undefined,
|
||||
to: context?.to,
|
||||
accountId: context?.accountId,
|
||||
threadId: context?.threadId,
|
||||
};
|
||||
}
|
||||
|
||||
export async function deliverSessionMaintenanceWarning(params: WarningParams): Promise<void> {
|
||||
if (!shouldSendWarning()) {
|
||||
return;
|
||||
@@ -85,10 +103,7 @@ export async function deliverSessionMaintenanceWarning(params: WarningParams): P
|
||||
warnedContexts.set(params.sessionKey, contextKey);
|
||||
|
||||
const text = buildWarningText(params.warning);
|
||||
const target = resolveSessionDeliveryTarget({
|
||||
entry: params.entry,
|
||||
requestedChannel: "last",
|
||||
});
|
||||
const target = resolveWarningDeliveryTarget(params.entry);
|
||||
|
||||
if (!target.channel || !target.to) {
|
||||
enqueueSystemEvent(text, { sessionKey: params.sessionKey });
|
||||
|
||||
Reference in New Issue
Block a user