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:
Tarun Sukhani
2026-02-04 15:14:46 +00:00
parent 7cfd0aed5f
commit e65d1deedd
59 changed files with 7326 additions and 310 deletions

View File

@@ -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;

View File

@@ -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",
),
);

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -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,