mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-28 08:52:45 +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";
|
type DiscordSessionKeyContext = {
|
||||||
import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime";
|
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(
|
export function normalizeExplicitDiscordSessionKey(
|
||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
ctx: Pick<MsgContext, "ChatType" | "From" | "SenderId">,
|
ctx: DiscordSessionKeyContext,
|
||||||
): string {
|
): string {
|
||||||
let normalized = sessionKey.trim().toLowerCase();
|
let normalized = sessionKey.trim().toLowerCase();
|
||||||
if (normalizeChatType(ctx.ChatType) !== "direct") {
|
if (normalizeDiscordChatType(ctx.ChatType) !== "direct") {
|
||||||
return normalized;
|
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 { computeBackoff, type BackoffPolicy } from "../infra/backoff.js";
|
||||||
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
|
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
|
||||||
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
||||||
|
import { lookupCachedContextTokens, MODEL_CONTEXT_TOKEN_CACHE } from "./context-cache.js";
|
||||||
import { normalizeProviderId } from "./model-selection.js";
|
import { normalizeProviderId } from "./model-selection.js";
|
||||||
|
|
||||||
type ModelEntry = { id: string; contextWindow?: number };
|
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 loadPromise: Promise<void> | null = null;
|
||||||
let configuredConfig: OpenClawConfig | undefined;
|
let configuredConfig: OpenClawConfig | undefined;
|
||||||
let configLoadFailures = 0;
|
let configLoadFailures = 0;
|
||||||
@@ -169,7 +169,7 @@ function primeConfiguredContextWindows(): OpenClawConfig | undefined {
|
|||||||
try {
|
try {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
applyConfiguredContextWindows({
|
applyConfiguredContextWindows({
|
||||||
cache: MODEL_CACHE,
|
cache: MODEL_CONTEXT_TOKEN_CACHE,
|
||||||
modelsConfig: cfg.models as ModelsConfig | undefined,
|
modelsConfig: cfg.models as ModelsConfig | undefined,
|
||||||
});
|
});
|
||||||
configuredConfig = cfg;
|
configuredConfig = cfg;
|
||||||
@@ -213,7 +213,7 @@ function ensureContextWindowCacheLoaded(): Promise<void> {
|
|||||||
? modelRegistry.getAvailable()
|
? modelRegistry.getAvailable()
|
||||||
: modelRegistry.getAll();
|
: modelRegistry.getAll();
|
||||||
applyDiscoveredContextWindows({
|
applyDiscoveredContextWindows({
|
||||||
cache: MODEL_CACHE,
|
cache: MODEL_CONTEXT_TOKEN_CACHE,
|
||||||
models,
|
models,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@@ -221,7 +221,7 @@ function ensureContextWindowCacheLoaded(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyConfiguredContextWindows({
|
applyConfiguredContextWindows({
|
||||||
cache: MODEL_CACHE,
|
cache: MODEL_CONTEXT_TOKEN_CACHE,
|
||||||
modelsConfig: cfg.models as ModelsConfig | undefined,
|
modelsConfig: cfg.models as ModelsConfig | undefined,
|
||||||
});
|
});
|
||||||
})().catch(() => {
|
})().catch(() => {
|
||||||
@@ -241,7 +241,7 @@ export function lookupContextTokens(
|
|||||||
if (options?.allowAsyncLoad !== false) {
|
if (options?.allowAsyncLoad !== false) {
|
||||||
void ensureContextWindowCacheLoaded();
|
void ensureContextWindowCacheLoaded();
|
||||||
}
|
}
|
||||||
return MODEL_CACHE.get(modelId);
|
return lookupCachedContextTokens(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldEagerWarmContextWindowCache()) {
|
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,
|
listChatCommandsForConfig,
|
||||||
normalizeCommandBody,
|
normalizeCommandBody,
|
||||||
} from "./commands-registry.js";
|
} from "./commands-registry.js";
|
||||||
import { isAbortTrigger } from "./reply/abort.js";
|
import { isAbortTrigger } from "./reply/abort-primitives.js";
|
||||||
|
|
||||||
export function hasControlCommand(
|
export function hasControlCommand(
|
||||||
text?: string,
|
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 type { SessionEntry } from "../../config/sessions/types.js";
|
||||||
import { updateSessionStore } from "../../config/sessions.js";
|
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
|
|
||||||
export type AbortCutoff = {
|
export type AbortCutoff = {
|
||||||
@@ -51,36 +50,6 @@ export function applyAbortCutoffToSessionEntry(
|
|||||||
entry.abortCutoffTimestamp = cutoff?.timestamp;
|
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 {
|
function toNumericMessageSid(value: string | undefined): bigint | undefined {
|
||||||
const trimmed = value?.trim();
|
const trimmed = value?.trim();
|
||||||
if (!trimmed || !/^\d+$/.test(trimmed)) {
|
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 { logVerbose } from "../../globals.js";
|
||||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||||
import { normalizeCommandBody, type CommandNormalizeOptions } from "../commands-registry.js";
|
|
||||||
import type { FinalizedMsgContext, MsgContext } from "../templating.js";
|
import type { FinalizedMsgContext, MsgContext } from "../templating.js";
|
||||||
import {
|
import {
|
||||||
applyAbortCutoffToSessionEntry,
|
applyAbortCutoffToSessionEntry,
|
||||||
resolveAbortCutoffFromContext,
|
resolveAbortCutoffFromContext,
|
||||||
shouldPersistAbortCutoff,
|
shouldPersistAbortCutoff,
|
||||||
} from "./abort-cutoff.js";
|
} from "./abort-cutoff.js";
|
||||||
|
import {
|
||||||
|
getAbortMemory,
|
||||||
|
getAbortMemorySizeForTest,
|
||||||
|
isAbortRequestText,
|
||||||
|
isAbortTrigger,
|
||||||
|
resetAbortMemoryForTest,
|
||||||
|
setAbortMemory,
|
||||||
|
} from "./abort-primitives.js";
|
||||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||||
import { clearSessionQueues } from "./queue.js";
|
import { clearSessionQueues } from "./queue.js";
|
||||||
|
|
||||||
export { resolveAbortCutoffFromContext, shouldSkipMessageByAbortCutoff } from "./abort-cutoff.js";
|
export { resolveAbortCutoffFromContext, shouldSkipMessageByAbortCutoff } from "./abort-cutoff.js";
|
||||||
|
export {
|
||||||
const ABORT_TRIGGERS = new Set([
|
getAbortMemory,
|
||||||
"stop",
|
getAbortMemorySizeForTest,
|
||||||
"esc",
|
isAbortRequestText,
|
||||||
"abort",
|
isAbortTrigger,
|
||||||
"wait",
|
resetAbortMemoryForTest,
|
||||||
"exit",
|
setAbortMemory,
|
||||||
"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 function formatAbortReplyText(stoppedSubagents?: number): string {
|
export function formatAbortReplyText(stoppedSubagents?: number): string {
|
||||||
if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
|
if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SessionEntry } from "../../config/sessions.js";
|
import type { SessionEntry } from "../../config/sessions.js";
|
||||||
import { updateSessionStore } 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: {
|
export async function applySessionHints(params: {
|
||||||
baseBody: string;
|
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})`;
|
return `${auth.label} (${auth.source})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resolveProfileOverride = (params: {
|
export { resolveProfileOverride } from "./directive-handling.auth-profile.js";
|
||||||
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 };
|
|
||||||
};
|
|
||||||
|
|||||||
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 { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||||
import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js";
|
import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js";
|
||||||
import type { ReplyPayload } from "../types.js";
|
import type { ReplyPayload } from "../types.js";
|
||||||
import {
|
import { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js";
|
||||||
maybeHandleModelDirectiveInfo,
|
import { maybeHandleModelDirectiveInfo } from "./directive-handling.model.js";
|
||||||
resolveModelSelectionFromDirective,
|
|
||||||
} from "./directive-handling.model.js";
|
|
||||||
import type { HandleDirectiveOnlyParams } from "./directive-handling.params.js";
|
import type { HandleDirectiveOnlyParams } from "./directive-handling.params.js";
|
||||||
import { maybeHandleQueueDirective } from "./directive-handling.queue-validation.js";
|
import { maybeHandleQueueDirective } from "./directive-handling.queue-validation.js";
|
||||||
import {
|
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,
|
resolveDefaultAgentId,
|
||||||
resolveSessionAgentId,
|
resolveSessionAgentId,
|
||||||
} from "../../agents/agent-scope.js";
|
} 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 { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||||
import {
|
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||||
buildModelAliasIndex,
|
|
||||||
type ModelAliasIndex,
|
|
||||||
resolveDefaultModelForAgent,
|
|
||||||
} from "../../agents/model-selection.js";
|
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import { updateSessionStore } from "../../config/sessions/store.js";
|
import { updateSessionStore } from "../../config/sessions/store.js";
|
||||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-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 type { InlineDirectives } from "./directive-handling.parse.js";
|
||||||
import { enqueueModeSwitchEvents } from "./directive-handling.shared.js";
|
import { enqueueModeSwitchEvents } from "./directive-handling.shared.js";
|
||||||
import type { ElevatedLevel, ReasoningLevel } from "./directives.js";
|
import type { ElevatedLevel, ReasoningLevel } from "./directives.js";
|
||||||
@@ -203,24 +199,7 @@ export async function persistInlineDirectives(params: {
|
|||||||
return {
|
return {
|
||||||
provider,
|
provider,
|
||||||
model,
|
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 * from "./directive-handling.impl.js";
|
||||||
export type { InlineDirectives } from "./directive-handling.parse.js";
|
export type { InlineDirectives } from "./directive-handling.parse.js";
|
||||||
export { isDirectiveOnly, parseInlineDirectives } 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";
|
export { formatDirectiveAck } from "./directive-handling.shared.js";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { collectTextContentBlocks } from "../../agents/content-blocks.js";
|
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 { BlockReplyChunking } from "../../agents/pi-embedded-block-chunker.js";
|
||||||
import type { SkillCommandSpec } from "../../agents/skills.js";
|
import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||||
import { applyOwnerOnlyToolPolicy } from "../../agents/tool-policy.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 { resolveGatewayMessageChannel } from "../../utils/message-channel.js";
|
||||||
import {
|
import {
|
||||||
listReservedChatSlashCommandNames,
|
listReservedChatSlashCommandNames,
|
||||||
listSkillCommandsForWorkspace,
|
|
||||||
resolveSkillCommandInvocation,
|
resolveSkillCommandInvocation,
|
||||||
} from "../skill-commands.js";
|
} from "../skill-commands-base.js";
|
||||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
||||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||||
import {
|
import {
|
||||||
clearAbortCutoffInSession,
|
|
||||||
readAbortCutoffFromSessionEntry,
|
readAbortCutoffFromSessionEntry,
|
||||||
resolveAbortCutoffFromContext,
|
resolveAbortCutoffFromContext,
|
||||||
shouldSkipMessageByAbortCutoff,
|
shouldSkipMessageByAbortCutoff,
|
||||||
} from "./abort-cutoff.js";
|
} from "./abort-cutoff.js";
|
||||||
import { getAbortMemory, isAbortRequestText } from "./abort.js";
|
import { getAbortMemory, isAbortRequestText } from "./abort-primitives.js";
|
||||||
import { buildStatusReply, handleCommands } from "./commands.js";
|
import type { buildStatusReply, handleCommands } from "./commands.runtime.js";
|
||||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||||
import { isDirectiveOnly } from "./directive-handling.parse.js";
|
import { isDirectiveOnly } from "./directive-handling.parse.js";
|
||||||
import type { createModelSelectionState } from "./model-selection.js";
|
import type { createModelSelectionState } from "./model-selection.js";
|
||||||
@@ -191,7 +188,7 @@ export async function handleInlineActions(params: {
|
|||||||
shouldLoadSkillCommands && params.skillCommands
|
shouldLoadSkillCommands && params.skillCommands
|
||||||
? params.skillCommands
|
? params.skillCommands
|
||||||
: shouldLoadSkillCommands
|
: shouldLoadSkillCommands
|
||||||
? listSkillCommandsForWorkspace({
|
? (await import("../skill-commands.runtime.js")).listSkillCommandsForWorkspace({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
cfg,
|
cfg,
|
||||||
skillFilter,
|
skillFilter,
|
||||||
@@ -222,6 +219,7 @@ export async function handleInlineActions(params: {
|
|||||||
resolveGatewayMessageChannel(ctx.Provider) ??
|
resolveGatewayMessageChannel(ctx.Provider) ??
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
|
const { createOpenClawTools } = await import("../../agents/openclaw-tools.runtime.js");
|
||||||
const tools = createOpenClawTools({
|
const tools = createOpenClawTools({
|
||||||
agentSessionKey: sessionKey,
|
agentSessionKey: sessionKey,
|
||||||
agentChannel: channel,
|
agentChannel: channel,
|
||||||
@@ -305,7 +303,9 @@ export async function handleInlineActions(params: {
|
|||||||
return { kind: "reply", reply: undefined };
|
return { kind: "reply", reply: undefined };
|
||||||
}
|
}
|
||||||
if (cutoff) {
|
if (cutoff) {
|
||||||
await clearAbortCutoffInSession({
|
await (
|
||||||
|
await import("./abort-cutoff.runtime.js")
|
||||||
|
).clearAbortCutoffInSessionRuntime({
|
||||||
sessionEntry,
|
sessionEntry,
|
||||||
sessionStore,
|
sessionStore,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
@@ -335,6 +335,7 @@ export async function handleInlineActions(params: {
|
|||||||
isGroup,
|
isGroup,
|
||||||
}) && inlineStatusRequested;
|
}) && inlineStatusRequested;
|
||||||
if (handleInlineStatus) {
|
if (handleInlineStatus) {
|
||||||
|
const { buildStatusReply } = await import("./commands.runtime.js");
|
||||||
const inlineStatusReply = await buildStatusReply({
|
const inlineStatusReply = await buildStatusReply({
|
||||||
cfg,
|
cfg,
|
||||||
command,
|
command,
|
||||||
@@ -358,8 +359,9 @@ export async function handleInlineActions(params: {
|
|||||||
directives = { ...directives, hasStatusDirective: false };
|
directives = { ...directives, hasStatusDirective: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const runCommands = (commandInput: typeof command) =>
|
const runCommands = async (commandInput: typeof command) => {
|
||||||
handleCommands({
|
const { handleCommands } = await import("./commands.runtime.js");
|
||||||
|
return handleCommands({
|
||||||
// Pass sessionCtx so command handlers can mutate stripped body for same-turn continuation.
|
// Pass sessionCtx so command handlers can mutate stripped body for same-turn continuation.
|
||||||
ctx: sessionCtx,
|
ctx: sessionCtx,
|
||||||
// Keep original finalized context in sync when command handlers need outer-dispatch side effects.
|
// Keep original finalized context in sync when command handlers need outer-dispatch side effects.
|
||||||
@@ -397,6 +399,7 @@ export async function handleInlineActions(params: {
|
|||||||
skillCommands,
|
skillCommands,
|
||||||
typing,
|
typing,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (inlineCommand) {
|
if (inlineCommand) {
|
||||||
const inlineCommandContext = {
|
const inlineCommandContext = {
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import { resolveCommandAuthorization } from "../command-auth.js";
|
|||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||||
import { emitResetCommandHooks, type ResetCommandAction } from "./commands-core.js";
|
import { resolveDefaultModel } from "./directive-handling.defaults.js";
|
||||||
import { resolveDefaultModel } from "./directive-handling.persist.js";
|
|
||||||
import { resolveReplyDirectives } from "./get-reply-directives.js";
|
import { resolveReplyDirectives } from "./get-reply-directives.js";
|
||||||
import { handleInlineActions } from "./get-reply-inline-actions.js";
|
import { handleInlineActions } from "./get-reply-inline-actions.js";
|
||||||
import { runPreparedReply } from "./get-reply-run.js";
|
import { runPreparedReply } from "./get-reply-run.js";
|
||||||
@@ -31,6 +30,8 @@ function shouldLogCoreIngressTiming(): boolean {
|
|||||||
return process.env.OPENCLAW_DEBUG_INGRESS_TIMING === "1";
|
return process.env.OPENCLAW_DEBUG_INGRESS_TIMING === "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResetCommandAction = "new" | "reset";
|
||||||
|
|
||||||
function mergeSkillFilters(channelFilter?: string[], agentFilter?: string[]): string[] | undefined {
|
function mergeSkillFilters(channelFilter?: string[], agentFilter?: string[]): string[] | undefined {
|
||||||
const normalize = (list?: string[]) => {
|
const normalize = (list?: string[]) => {
|
||||||
if (!Array.isArray(list)) {
|
if (!Array.isArray(list)) {
|
||||||
@@ -355,6 +356,7 @@ export async function getReplyFromConfig(
|
|||||||
if (!resetMatch) {
|
if (!resetMatch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { emitResetCommandHooks } = await import("./commands-core.runtime.js");
|
||||||
const action: ResetCommandAction = resetMatch[1] === "reset" ? "reset" : "new";
|
const action: ResetCommandAction = resetMatch[1] === "reset" ? "reset" : "new";
|
||||||
await emitResetCommandHooks({
|
await emitResetCommandHooks({
|
||||||
action,
|
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 type { OpenClawConfig } from "../../config/config.js";
|
||||||
import { resolveSessionFilePath } from "../../config/sessions/paths.js";
|
|
||||||
import type { SessionEntry } from "../../config/sessions/types.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;
|
return DEFAULT_PARENT_FORK_MAX_TOKENS;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function forkSessionFromParent(params: {
|
export async function forkSessionFromParent(params: {
|
||||||
parentEntry: SessionEntry;
|
parentEntry: SessionEntry;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
sessionsDir: string;
|
sessionsDir: string;
|
||||||
}): { sessionId: string; sessionFile: string } | null {
|
}): Promise<{ sessionId: string; sessionFile: string } | null> {
|
||||||
const parentSessionFile = resolveSessionFilePath(
|
const runtime = await import("./session-fork.runtime.js");
|
||||||
params.parentEntry.sessionId,
|
return runtime.forkSessionFromParentRuntime(params);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ export async function initSessionState(params: {
|
|||||||
`forking from parent session: parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
|
`forking from parent session: parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
|
||||||
`parentTokens=${parentTokens}`,
|
`parentTokens=${parentTokens}`,
|
||||||
);
|
);
|
||||||
const forked = forkSessionFromParent({
|
const forked = await forkSessionFromParent({
|
||||||
parentEntry: sessionStore[parentSessionKey],
|
parentEntry: sessionStore[parentSessionKey],
|
||||||
agentId,
|
agentId,
|
||||||
sessionsDir: path.dirname(storePath),
|
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 {
|
export { listSkillCommandsForAgents, listSkillCommandsForWorkspace } from "./skill-commands.js";
|
||||||
listReservedChatSlashCommandNames,
|
|
||||||
listSkillCommandsForWorkspace,
|
|
||||||
resolveSkillCommandInvocation,
|
|
||||||
} from "./skill-commands.js";
|
|
||||||
|
|||||||
@@ -8,30 +8,11 @@ import { buildWorkspaceSkillCommandSpecs, type SkillCommandSpec } from "../agent
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { logVerbose } from "../globals.js";
|
import { logVerbose } from "../globals.js";
|
||||||
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
||||||
import { listChatCommands } from "./commands-registry.js";
|
import { listReservedChatSlashCommandNames } from "./skill-commands-base.js";
|
||||||
|
export {
|
||||||
export function listReservedChatSlashCommandNames(extraNames: string[] = []): Set<string> {
|
listReservedChatSlashCommandNames,
|
||||||
const reserved = new Set<string>();
|
resolveSkillCommandInvocation,
|
||||||
for (const command of listChatCommands()) {
|
} from "./skill-commands-base.js";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listSkillCommandsForWorkspace(params: {
|
export function listSkillCommandsForWorkspace(params: {
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
@@ -131,74 +112,3 @@ export function listSkillCommandsForAgents(params: {
|
|||||||
export const __testing = {
|
export const __testing = {
|
||||||
dedupeBySkillName,
|
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 = {
|
export type ChatAbortControllerEntry = {
|
||||||
controller: AbortController;
|
controller: AbortController;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
resolveSessionAgentId: vi.fn(() => "agent-from-key"),
|
resolveSessionAgentId: vi.fn(() => "agent-from-key"),
|
||||||
resolveSessionDeliveryTarget: vi.fn(() => ({
|
deliveryContextFromSession: vi.fn(() => ({
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
to: "+15550001",
|
to: "+15550001",
|
||||||
accountId: "acct-1",
|
accountId: "acct-1",
|
||||||
@@ -50,7 +50,7 @@ describe("deliverSessionMaintenanceWarning", () => {
|
|||||||
process.env.NODE_ENV = "development";
|
process.env.NODE_ENV = "development";
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
mocks.resolveSessionAgentId.mockClear();
|
mocks.resolveSessionAgentId.mockClear();
|
||||||
mocks.resolveSessionDeliveryTarget.mockClear();
|
mocks.deliveryContextFromSession.mockClear();
|
||||||
mocks.normalizeMessageChannel.mockClear();
|
mocks.normalizeMessageChannel.mockClear();
|
||||||
mocks.isDeliverableMessageChannel.mockClear();
|
mocks.isDeliverableMessageChannel.mockClear();
|
||||||
mocks.deliverOutboundPayloads.mockClear();
|
mocks.deliverOutboundPayloads.mockClear();
|
||||||
@@ -62,10 +62,10 @@ describe("deliverSessionMaintenanceWarning", () => {
|
|||||||
normalizeMessageChannel: mocks.normalizeMessageChannel,
|
normalizeMessageChannel: mocks.normalizeMessageChannel,
|
||||||
isDeliverableMessageChannel: mocks.isDeliverableMessageChannel,
|
isDeliverableMessageChannel: mocks.isDeliverableMessageChannel,
|
||||||
}));
|
}));
|
||||||
vi.doMock("./outbound/targets.js", () => ({
|
vi.doMock("../utils/delivery-context.js", () => ({
|
||||||
resolveSessionDeliveryTarget: mocks.resolveSessionDeliveryTarget,
|
deliveryContextFromSession: mocks.deliveryContextFromSession,
|
||||||
}));
|
}));
|
||||||
vi.doMock("./outbound/deliver.js", () => ({
|
vi.doMock("./outbound/deliver-runtime.js", () => ({
|
||||||
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
|
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
|
||||||
}));
|
}));
|
||||||
vi.doMock("./system-events.js", () => ({
|
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 () => {
|
it("falls back to a system event when the last target is not deliverable", async () => {
|
||||||
mocks.resolveSessionDeliveryTarget.mockReturnValueOnce({
|
mocks.deliveryContextFromSession.mockReturnValueOnce({
|
||||||
channel: "debug",
|
channel: "debug",
|
||||||
to: "+15550001",
|
to: "+15550001",
|
||||||
accountId: "acct-1",
|
accountId: "acct-1",
|
||||||
@@ -143,7 +143,7 @@ describe("deliverSessionMaintenanceWarning", () => {
|
|||||||
|
|
||||||
await deliverSessionMaintenanceWarning(createParams());
|
await deliverSessionMaintenanceWarning(createParams());
|
||||||
|
|
||||||
expect(mocks.resolveSessionDeliveryTarget).not.toHaveBeenCalled();
|
expect(mocks.deliveryContextFromSession).not.toHaveBeenCalled();
|
||||||
expect(mocks.deliverOutboundPayloads).not.toHaveBeenCalled();
|
expect(mocks.deliverOutboundPayloads).not.toHaveBeenCalled();
|
||||||
expect(mocks.enqueueSystemEvent).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 { SessionMaintenanceWarning } from "../config/sessions/store-maintenance.js";
|
||||||
import type { SessionEntry } from "../config/sessions/types.js";
|
import type { SessionEntry } from "../config/sessions/types.js";
|
||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
|
import { deliveryContextFromSession } from "../utils/delivery-context.js";
|
||||||
import { isDeliverableMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js";
|
import { isDeliverableMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js";
|
||||||
import { buildOutboundSessionContext } from "./outbound/session-context.js";
|
import { buildOutboundSessionContext } from "./outbound/session-context.js";
|
||||||
import { resolveSessionDeliveryTarget } from "./outbound/targets.js";
|
|
||||||
import { enqueueSystemEvent } from "./system-events.js";
|
import { enqueueSystemEvent } from "./system-events.js";
|
||||||
|
|
||||||
type WarningParams = {
|
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> {
|
export async function deliverSessionMaintenanceWarning(params: WarningParams): Promise<void> {
|
||||||
if (!shouldSendWarning()) {
|
if (!shouldSendWarning()) {
|
||||||
return;
|
return;
|
||||||
@@ -85,10 +103,7 @@ export async function deliverSessionMaintenanceWarning(params: WarningParams): P
|
|||||||
warnedContexts.set(params.sessionKey, contextKey);
|
warnedContexts.set(params.sessionKey, contextKey);
|
||||||
|
|
||||||
const text = buildWarningText(params.warning);
|
const text = buildWarningText(params.warning);
|
||||||
const target = resolveSessionDeliveryTarget({
|
const target = resolveWarningDeliveryTarget(params.entry);
|
||||||
entry: params.entry,
|
|
||||||
requestedChannel: "last",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!target.channel || !target.to) {
|
if (!target.channel || !target.to) {
|
||||||
enqueueSystemEvent(text, { sessionKey: params.sessionKey });
|
enqueueSystemEvent(text, { sessionKey: params.sessionKey });
|
||||||
|
|||||||
Reference in New Issue
Block a user