chore: merge origin/main into mintlify/af82bda4 and resolve conflicts

This commit is contained in:
mintlify[bot]
2026-09-16 11:46:52 +00:00
committed by GitHub
parent b5ae772eae
commit 08d9636491
693 changed files with 55668 additions and 22676 deletions
+13 -145
View File
@@ -43,18 +43,9 @@ import {
} from "./isolation.ts";
import {
loadCompactTriagePrompt,
loadDreamPrompt,
isSkillsMode,
} from "./skill-loader.ts";
import { recall as skillRecall, sanitizeQuery } from "./recall.ts";
import {
incrementSessionCount,
checkCheapGates,
checkMemoryGate,
acquireDreamLock,
releaseDreamLock,
recordDreamCompletion,
} from "./dream-gate.ts";
import { PlatformBackend } from "./backend/platform.ts";
import type { Backend } from "./backend/base.ts";
import { registerCliCommands } from "./cli/commands.ts";
@@ -62,6 +53,7 @@ import { readPluginAuth } from "./cli/config-file.ts";
import { registerAllTools } from "./tools/index.ts";
import type { ToolDeps } from "./tools/index.ts";
import { captureEvent } from "./telemetry.ts";
import { createMemoryLifecycle } from "../agent-plugin-core/typescript/src/lifecycle.ts";
import { bootstrapTelemetryFlag } from "./fs-safe.ts";
// ============================================================================
@@ -176,6 +168,8 @@ const memoryPlugin = definePluginEntry({
}
const provider = createProvider(cfg, api);
const lifecycle = createMemoryLifecycle();
lifecycle.beginSession();
// Create Backend instance — PlatformBackend for platform mode, providerToBackend adapter for OSS
let backend: Backend;
@@ -220,10 +214,6 @@ const memoryPlugin = definePluginEntry({
api.registerMemoryCapability({
publicArtifacts: createPublicArtifactsProvider({
provider,
cfg,
get stateDir() {
return pluginStateDir;
},
effectiveUserId: _effectiveUserId,
}),
runtime: {
@@ -373,10 +363,10 @@ const memoryPlugin = definePluginEntry({
setCurrentSessionId: (id: string) => {
currentSessionId = id;
},
getStateDir: () => pluginStateDir,
},
skillsActive,
_captureEvent,
lifecycle,
);
// ========================================================================
@@ -420,13 +410,13 @@ function registerHooks(
) => SearchOptions,
session: {
setCurrentSessionId: (id: string) => void;
getStateDir: () => string | undefined;
},
skillsActive: boolean = false,
_captureEvent: (
event: string,
props?: Record<string, unknown>,
) => void = () => {},
lifecycle: ReturnType<typeof createMemoryLifecycle> = createMemoryLifecycle(),
) {
// ========================================================================
// SKILLS MODE: Agentic memory via before_prompt_build
@@ -493,7 +483,7 @@ function registerHooks(
if (recallEnabled && recallStrategy !== "manual") {
const recallStart = Date.now();
try {
const query = sanitizeQuery(event.prompt);
const query = lifecycle.prepareUserText(sanitizeQuery(event.prompt));
// Smart mode: skip session search (saves 1 API call per turn)
const sessionIdForRecall =
@@ -533,142 +523,18 @@ function registerHooks(
);
}
// Auto-dream: check CHEAP gates first (local file reads only).
// Only hit the API for memory count if time + session gates pass.
const stateDir = session.getStateDir();
const dreamEnabled =
cfg.skills?.dream?.enabled !== false &&
cfg.skills?.dream?.auto !== false;
let dreamContext = "";
if (dreamEnabled && stateDir && !isSubagent) {
try {
const cheapResult = checkCheapGates(
stateDir,
cfg.skills?.dream ?? {},
);
if (cheapResult.proceed) {
// Cheap gates passed. Now do the expensive memory count check.
const memories = await provider.getAll({
user_id: userId,
source: "OPENCLAW",
});
const memCount = Array.isArray(memories) ? memories.length : 0;
const memResult = checkMemoryGate(
memCount,
cfg.skills?.dream ?? {},
);
if (memResult.pass && acquireDreamLock(stateDir)) {
const dreamPrompt = loadDreamPrompt(cfg.skills ?? {});
if (dreamPrompt) {
dreamContext =
"\n\n<auto-dream>\nIMPORTANT: Before responding to the user, run a memory consolidation pass. Follow the protocol below, then respond normally.\n\n" +
dreamPrompt +
"\n</auto-dream>";
// Track which session triggered dream (session-keyed, not global)
dreamSessionId = sessionId;
_captureEvent("openclaw.hook.dream", {
phase: "triggered",
memory_count: memCount,
});
api.logger.info(
`openclaw-mem0: auto-dream triggered (${memCount} memories, gate passed)`,
);
} else {
releaseDreamLock(stateDir);
}
}
}
} catch (err) {
api.logger.warn(
`openclaw-mem0: auto-dream gate check failed: ${String(err)}`,
);
}
}
return {
prependSystemContext: systemContext, // cached by provider
prependContext: recallContext + dreamContext, // per-turn dynamic
prependContext: recallContext, // per-turn dynamic
};
});
// Session-keyed dream tracking. Only the session that triggered dream
// can complete it. Prevents cross-session false completion.
let dreamSessionId: string | undefined;
api.on("agent_end", async (event: any, ctx: any) => {
const sessionId = ctx?.sessionKey ?? undefined;
const trigger = ctx?.trigger ?? undefined;
if (sessionId) session.setCurrentSessionId(sessionId);
// If dream was triggered for THIS session, handle cleanup regardless
// of success/failure. A failed turn must still release the lock.
const stateDir = session.getStateDir();
if (dreamSessionId && dreamSessionId === sessionId && stateDir) {
dreamSessionId = undefined;
if (!event.success) {
// Turn failed/aborted after lock acquired. Release lock, do not
// record completion. Gates will re-trigger next eligible turn.
releaseDreamLock(stateDir);
api.logger.warn(
"openclaw-mem0: auto-dream turn failed, lock released, will retry",
);
return;
}
// Verify the model actually performed WRITE operations (not just reads).
// Only count memory_add, memory_update, memory_delete.
// Exclude memory_list and memory_search (read-only, orient-only pass).
// Scan only the LAST assistant message (this turn), not the full session
// snapshot, to avoid matching earlier tool calls from prior turns.
const WRITE_TOOLS = new Set([
"memory_add",
"memory_update",
"memory_delete",
]);
const messages = event.messages ?? [];
// Find the last assistant message (this turn's output)
const lastAssistant = [...messages]
.reverse()
.find((m: any) => m.role === "assistant");
const writeToolUsed =
lastAssistant && Array.isArray(lastAssistant.content)
? lastAssistant.content.some(
(block: any) =>
block.type === "tool_use" && WRITE_TOOLS.has(block.name),
)
: false;
if (writeToolUsed) {
releaseDreamLock(stateDir);
recordDreamCompletion(stateDir);
_captureEvent("openclaw.hook.dream", {
phase: "completed",
write_tools_used: true,
});
api.logger.info(
"openclaw-mem0: auto-dream completed (verified write tool usage), lock released",
);
} else {
releaseDreamLock(stateDir);
api.logger.warn(
"openclaw-mem0: auto-dream injected but no write tools executed. Lock released, will retry.",
);
}
return;
}
if (!event.success) return;
// Track session for dream gating (interactive turns only)
if (
stateDir &&
sessionId &&
!isNonInteractiveTrigger(trigger, sessionId)
) {
incrementSessionCount(stateDir, sessionId);
}
api.logger.info("openclaw-mem0: skills-mode agent_end (no auto-capture)");
});
@@ -733,6 +599,7 @@ function registerHooks(
"",
)
.trim();
const safePrompt = lifecycle.prepareUserText(cleanPrompt);
const recallStart = Date.now();
const recallWork = async () => {
@@ -741,7 +608,7 @@ function registerHooks(
// Search long-term memories (user-scoped; subagents read from parent namespace)
let longTermResults = await provider.search(
cleanPrompt,
safePrompt,
buildSearchOptions(
undefined,
recallTopK,
@@ -767,7 +634,7 @@ function registerHooks(
// Only broaden for genuinely new sessions with short prompts
// (cold-start blindness). Skip on subsequent turns to save API calls.
if (isNewSession && cleanPrompt.length < 100) {
if (isNewSession && safePrompt.length < 100) {
const broadOpts = buildSearchOptions(
undefined,
5,
@@ -1007,8 +874,9 @@ function registerHooks(
content: m.content,
}));
// Apply noise filtering pipeline: drop noise, strip fragments, truncate
const formattedMessages = filterMessagesForExtraction(selected);
// Filter noise and redact secrets without truncating message text.
const formattedMessages: Array<{ role: string; content: string }> =
lifecycle.prepareConversation(filterMessagesForExtraction(selected));
if (formattedMessages.length === 0) return;