mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 23:44:27 +00:00
Sync adabot changes on top of origin/main
Includes: - memory-neo4j: four-phase sleep cycle (dedup, decay, extraction, cleanup) - memory-neo4j: full plugin implementation with hybrid search - memory-lancedb: updates and benchmarks - OpenSpec workflow skills and commands - Session memory hooks - Various CLI and config improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -111,8 +111,8 @@ function assertCommandRegistry(commands: ChatCommandDefinition[]): void {
|
||||
}
|
||||
|
||||
for (const alias of command.textAliases) {
|
||||
if (!alias.startsWith("/")) {
|
||||
throw new Error(`Command alias missing leading '/': ${alias}`);
|
||||
if (!alias.startsWith("/") && !alias.startsWith(".")) {
|
||||
throw new Error(`Command alias missing leading '/' or '.': ${alias}`);
|
||||
}
|
||||
const aliasKey = alias.toLowerCase();
|
||||
if (textAliases.has(aliasKey)) {
|
||||
@@ -618,6 +618,8 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
||||
registerAlias(commands, "reasoning", "/reason");
|
||||
registerAlias(commands, "elevated", "/elev");
|
||||
registerAlias(commands, "steer", "/tell");
|
||||
registerAlias(commands, "model", ".model");
|
||||
registerAlias(commands, "models", ".models");
|
||||
|
||||
assertCommandRegistry(commands);
|
||||
return commands;
|
||||
|
||||
@@ -14,7 +14,7 @@ export function extractModelDirective(
|
||||
}
|
||||
|
||||
const modelMatch = body.match(
|
||||
/(?:^|\s)\/model(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)*)?/i,
|
||||
/(?:^|\s)[/.]model(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)*)?/i,
|
||||
);
|
||||
|
||||
const aliases = (options?.aliases ?? []).map((alias) => alias.trim()).filter(Boolean);
|
||||
@@ -23,7 +23,7 @@ export function extractModelDirective(
|
||||
? null
|
||||
: body.match(
|
||||
new RegExp(
|
||||
`(?:^|\\s)\\/(${aliases.map(escapeRegExp).join("|")})(?=$|\\s|:)(?:\\s*:\\s*)?`,
|
||||
`(?:^|\\s)[/.](${aliases.map(escapeRegExp).join("|")})(?=$|\\s|:)(?:\\s*:\\s*)?`,
|
||||
"i",
|
||||
),
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ const matchLevelDirective = (
|
||||
names: string[],
|
||||
): { start: number; end: number; rawLevel?: string } | null => {
|
||||
const namePattern = names.map(escapeRegExp).join("|");
|
||||
const match = body.match(new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)`, "i"));
|
||||
const match = body.match(new RegExp(`(?:^|\\s)[/.](?:${namePattern})(?=$|\\s|:)`, "i"));
|
||||
if (!match || match.index === undefined) {
|
||||
return null;
|
||||
}
|
||||
@@ -79,7 +79,7 @@ const extractSimpleDirective = (
|
||||
): { cleaned: string; hasDirective: boolean } => {
|
||||
const namePattern = names.map(escapeRegExp).join("|");
|
||||
const match = body.match(
|
||||
new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)(?:\\s*:\\s*)?`, "i"),
|
||||
new RegExp(`(?:^|\\s)[/.](?:${namePattern})(?=$|\\s|:)(?:\\s*:\\s*)?`, "i"),
|
||||
);
|
||||
const cleaned = match ? body.replace(match[0], " ").replace(/\s+/g, " ").trim() : body.trim();
|
||||
return {
|
||||
|
||||
@@ -172,7 +172,7 @@ export function extractExecDirective(body?: string): ExecDirectiveParse {
|
||||
invalidNode: false,
|
||||
};
|
||||
}
|
||||
const re = /(?:^|\s)\/exec(?=$|\s|:)/i;
|
||||
const re = /(?:^|\s)[/.]exec(?=$|\s|:)/i;
|
||||
const match = re.exec(body);
|
||||
if (!match) {
|
||||
return {
|
||||
@@ -185,8 +185,10 @@ export function extractExecDirective(body?: string): ExecDirectiveParse {
|
||||
invalidNode: false,
|
||||
};
|
||||
}
|
||||
const start = match.index + match[0].indexOf("/exec");
|
||||
const argsStart = start + "/exec".length;
|
||||
// Find the directive start (handle both /exec and .exec)
|
||||
const execMatch = match[0].match(/[/.]exec/i);
|
||||
const start = match.index + (execMatch ? match[0].indexOf(execMatch[0]) : 0);
|
||||
const argsStart = start + 5; // "/exec" or ".exec" is always 5 chars
|
||||
const parsed = parseExecDirectiveArgs(body.slice(argsStart));
|
||||
const cleanedRaw = `${body.slice(0, start)} ${body.slice(argsStart + parsed.consumed)}`;
|
||||
const cleaned = cleanedRaw.replace(/\s+/g, " ").trim();
|
||||
|
||||
@@ -262,6 +262,23 @@ export async function handleInlineActions(params: {
|
||||
sessionCtx.BodyForAgent = rewrittenBody;
|
||||
sessionCtx.BodyStripped = rewrittenBody;
|
||||
cleanedBody = rewrittenBody;
|
||||
|
||||
// Apply skill-level thinking/model overrides if configured
|
||||
if (skillInvocation.command.thinking) {
|
||||
directives = {
|
||||
...directives,
|
||||
hasThinkDirective: true,
|
||||
thinkLevel: skillInvocation.command.thinking,
|
||||
rawThinkLevel: skillInvocation.command.thinking,
|
||||
};
|
||||
}
|
||||
if (skillInvocation.command.model) {
|
||||
directives = {
|
||||
...directives,
|
||||
hasModelDirective: true,
|
||||
rawModelDirective: skillInvocation.command.model,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const sendInlineReply = async (reply?: ReplyPayload) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveAgentSkillsFilter,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { resolveModelRefFromString } from "../../agents/model-selection.js";
|
||||
import { compactEmbeddedPiSession } from "../../agents/pi-embedded-runner.js";
|
||||
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
||||
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../../agents/workspace.js";
|
||||
import { type OpenClawConfig, loadConfig } from "../../config/config.js";
|
||||
@@ -154,6 +155,7 @@ export async function getReplyFromConfig(
|
||||
sessionId,
|
||||
isNewSession,
|
||||
resetTriggered,
|
||||
compactTriggered,
|
||||
systemSent,
|
||||
abortedLastRun,
|
||||
storePath,
|
||||
@@ -293,6 +295,35 @@ export async function getReplyFromConfig(
|
||||
workspaceDir,
|
||||
});
|
||||
|
||||
// Handle compact trigger - force compaction without resetting session
|
||||
if (compactTriggered && sessionEntry.sessionFile) {
|
||||
try {
|
||||
const compactResult = await compactEmbeddedPiSession({
|
||||
sessionId: sessionEntry.sessionId,
|
||||
sessionFile: sessionEntry.sessionFile,
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
provider,
|
||||
model,
|
||||
});
|
||||
if (compactResult.compacted && compactResult.result) {
|
||||
const tokensBefore = compactResult.result.tokensBefore;
|
||||
const tokensAfter = compactResult.result.tokensAfter ?? 0;
|
||||
return {
|
||||
text: `✅ Context compacted successfully.\n\n**Before:** ${tokensBefore.toLocaleString()} tokens\n**After:** ${tokensAfter.toLocaleString()} tokens\n**Saved:** ${(tokensBefore - tokensAfter).toLocaleString()} tokens`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: `ℹ️ Nothing to compact. ${compactResult.reason ?? "Session is already compact."}`,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
text: `❌ Compaction failed: ${String(err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return runPreparedReply({
|
||||
ctx,
|
||||
sessionCtx,
|
||||
|
||||
@@ -44,6 +44,7 @@ export type SessionInitResult = {
|
||||
sessionId: string;
|
||||
isNewSession: boolean;
|
||||
resetTriggered: boolean;
|
||||
compactTriggered: boolean;
|
||||
systemSent: boolean;
|
||||
abortedLastRun: boolean;
|
||||
storePath: string;
|
||||
@@ -133,6 +134,7 @@ export async function initSessionState(params: {
|
||||
let systemSent = false;
|
||||
let abortedLastRun = false;
|
||||
let resetTriggered = false;
|
||||
let compactTriggered = false;
|
||||
|
||||
let persistedThinking: string | undefined;
|
||||
let persistedVerbose: string | undefined;
|
||||
@@ -198,6 +200,22 @@ export async function initSessionState(params: {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for compact triggers (e.g., ".compact", "/compact")
|
||||
const compactTriggers = sessionCfg?.compactTriggers ?? [];
|
||||
if (!resetTriggered && resetAuthorized) {
|
||||
for (const trigger of compactTriggers) {
|
||||
if (!trigger) {
|
||||
continue;
|
||||
}
|
||||
const triggerLower = trigger.toLowerCase();
|
||||
if (trimmedBodyLower === triggerLower || strippedForResetLower === triggerLower) {
|
||||
compactTriggered = true;
|
||||
bodyStripped = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionKey = resolveSessionKey(sessionScope, sessionCtxForState, mainKey);
|
||||
const entry = sessionStore[sessionKey];
|
||||
const previousSessionEntry = resetTriggered && entry ? { ...entry } : undefined;
|
||||
@@ -458,6 +476,7 @@ export async function initSessionState(params: {
|
||||
sessionId: sessionId ?? crypto.randomUUID(),
|
||||
isNewSession,
|
||||
resetTriggered,
|
||||
compactTriggered,
|
||||
systemSent,
|
||||
abortedLastRun,
|
||||
storePath,
|
||||
|
||||
Reference in New Issue
Block a user