mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-25 16:53:02 +08:00
Compare commits
8 Commits
v2026.6.9
...
refactor/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b7b9b9547 | ||
|
|
59b0825d8c | ||
|
|
b1b9f0094c | ||
|
|
9a6a663d84 | ||
|
|
db365d531f | ||
|
|
b469092d92 | ||
|
|
064ec5f07c | ||
|
|
c3f5af897c |
@@ -1,20 +1,5 @@
|
||||
// Real workspace contract for memory embedding providers and batch helpers.
|
||||
// Package-local memory embedding helpers.
|
||||
|
||||
export {
|
||||
getMemoryEmbeddingProvider,
|
||||
listRegisteredMemoryEmbeddingProviders,
|
||||
listMemoryEmbeddingProviders,
|
||||
listRegisteredMemoryEmbeddingProviderAdapters,
|
||||
} from "./host/openclaw-runtime-memory.js";
|
||||
export type {
|
||||
MemoryEmbeddingBatchChunk,
|
||||
MemoryEmbeddingBatchOptions,
|
||||
MemoryEmbeddingProvider,
|
||||
MemoryEmbeddingProviderAdapter,
|
||||
MemoryEmbeddingProviderCreateOptions,
|
||||
MemoryEmbeddingProviderCreateResult,
|
||||
MemoryEmbeddingProviderRuntime,
|
||||
} from "./host/openclaw-runtime-memory.js";
|
||||
export { createLocalEmbeddingProvider, DEFAULT_LOCAL_MODEL } from "./host/embeddings.js";
|
||||
export { extractBatchErrorMessage, formatUnavailableBatchError } from "./host/batch-error-utils.js";
|
||||
export { postJsonWithRetry } from "./host/batch-http.js";
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
// Real workspace contract for memory engine foundation concerns.
|
||||
// Package-local foundation exports. Core-only helpers are bound by the
|
||||
// workspace facade.
|
||||
|
||||
export {
|
||||
parseDurationMs,
|
||||
resolveAgentContextLimits,
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionAgentId,
|
||||
} from "./host/openclaw-runtime-agent.js";
|
||||
export {
|
||||
resolveMemorySearchConfig,
|
||||
resolveMemorySearchSyncConfig,
|
||||
type ResolvedMemorySearchConfig,
|
||||
type ResolvedMemorySearchSyncConfig,
|
||||
} from "./host/openclaw-runtime-agent.js";
|
||||
export { parseDurationMs } from "./host/openclaw-runtime-config.js";
|
||||
export { loadConfig } from "./host/openclaw-runtime-config.js";
|
||||
export { resolveStateDir } from "./host/openclaw-runtime-config.js";
|
||||
export { resolveSessionTranscriptsDirForAgent } from "./host/openclaw-runtime-config.js";
|
||||
export {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
} from "./host/openclaw-runtime-config.js";
|
||||
export { writeFileWithinRoot } from "./host/openclaw-runtime-io.js";
|
||||
export { createSubsystemLogger } from "./host/openclaw-runtime-io.js";
|
||||
export { detectMime } from "./host/openclaw-runtime-io.js";
|
||||
export { resolveGlobalSingleton } from "./host/openclaw-runtime-io.js";
|
||||
export { onSessionTranscriptUpdate } from "./host/openclaw-runtime-session.js";
|
||||
export { splitShellArgs } from "./host/openclaw-runtime-io.js";
|
||||
export { runTasksWithConcurrency } from "./host/openclaw-runtime-io.js";
|
||||
export {
|
||||
shortenHomeInString,
|
||||
shortenHomePath,
|
||||
resolveStateDir,
|
||||
resolveUserPath,
|
||||
truncateUtf16Safe,
|
||||
} from "./host/openclaw-runtime-io.js";
|
||||
export type { OpenClawConfig } from "./host/openclaw-runtime-config.js";
|
||||
export type { SessionSendPolicyConfig } from "./host/openclaw-runtime-config.js";
|
||||
export type { SecretInput } from "./host/openclaw-runtime-config.js";
|
||||
export type {
|
||||
MemoryBackend,
|
||||
MemoryCitationsMode,
|
||||
MemoryQmdConfig,
|
||||
MemoryQmdIndexPath,
|
||||
MemoryQmdMcporterConfig,
|
||||
MemoryQmdSearchMode,
|
||||
} from "./host/openclaw-runtime-config.js";
|
||||
export type { MemorySearchConfig } from "./host/openclaw-runtime-config.js";
|
||||
splitShellArgs,
|
||||
type MemoryBackend,
|
||||
type MemoryCitationsMode,
|
||||
type MemoryQmdConfig,
|
||||
type MemoryQmdIndexPath,
|
||||
type MemoryQmdMcporterConfig,
|
||||
type MemoryQmdSearchMode,
|
||||
type MemorySearchConfig,
|
||||
type OpenClawConfig,
|
||||
type SecretInput,
|
||||
type SessionSendPolicyConfig,
|
||||
} from "./host/config-utils.js";
|
||||
export {
|
||||
CHARS_PER_TOKEN_ESTIMATE,
|
||||
HEARTBEAT_PROMPT,
|
||||
HEARTBEAT_TOKEN,
|
||||
SILENT_REPLY_TOKEN,
|
||||
getMemoryHostServices,
|
||||
setMemoryHostServices,
|
||||
withMemoryHostServices,
|
||||
type MemoryHostServices,
|
||||
} from "./host/services.js";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Real workspace contract for QMD/session/query helpers used by the memory engine.
|
||||
|
||||
import { getMemoryHostServices } from "./host/services.js";
|
||||
|
||||
export { extractKeywords, isQueryStopWordToken } from "./host/query-expansion.js";
|
||||
export {
|
||||
buildSessionEntry,
|
||||
@@ -12,7 +14,8 @@ export {
|
||||
type SessionFileEntry,
|
||||
type SessionTranscriptClassification,
|
||||
} from "./host/session-files.js";
|
||||
export { parseUsageCountedSessionIdFromFileName } from "./host/openclaw-runtime-session.js";
|
||||
export const parseUsageCountedSessionIdFromFileName = (fileName: string): string | null =>
|
||||
getMemoryHostServices().session.parseUsageCountedSessionIdFromFileName(fileName);
|
||||
export { parseQmdQueryJson, type QmdQueryResult } from "./host/qmd-query-parser.js";
|
||||
export {
|
||||
deriveQmdScopeChannel,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { EmbeddingProviderOptions } from "./embeddings.types.js";
|
||||
import { requireApiKey, resolveApiKeyForProvider } from "./openclaw-runtime-auth.js";
|
||||
import { buildRemoteBaseUrlPolicy } from "./remote-http.js";
|
||||
import { resolveMemorySecretInputString } from "./secret-input.js";
|
||||
import { getMemoryHostServices } from "./services.js";
|
||||
import type { SsrFPolicy } from "./ssrf-policy.js";
|
||||
import { normalizeOptionalString } from "./string-utils.js";
|
||||
|
||||
@@ -19,10 +19,11 @@ export async function resolveRemoteEmbeddingBearerClient(params: {
|
||||
});
|
||||
const remoteBaseUrl = normalizeOptionalString(remote?.baseUrl);
|
||||
const providerConfig = params.options.config.models?.providers?.[params.provider];
|
||||
const auth = getMemoryHostServices().auth;
|
||||
const apiKey = remoteApiKey
|
||||
? remoteApiKey
|
||||
: requireApiKey(
|
||||
await resolveApiKeyForProvider({
|
||||
: auth.requireApiKey(
|
||||
await auth.resolveApiKeyForProvider({
|
||||
provider: params.provider,
|
||||
cfg: params.options.config,
|
||||
agentDir: params.options.agentDir,
|
||||
|
||||
@@ -12,16 +12,7 @@ import {
|
||||
type MemoryMultimodalModality,
|
||||
type MemoryMultimodalSettings,
|
||||
} from "./multimodal.js";
|
||||
import {
|
||||
CHARS_PER_TOKEN_ESTIMATE,
|
||||
detectMime,
|
||||
estimateStringChars,
|
||||
runTasksWithConcurrency,
|
||||
} from "./openclaw-runtime-io.js";
|
||||
import {
|
||||
resolveCanonicalRootMemoryFile,
|
||||
shouldSkipRootMemoryAuxiliaryPath,
|
||||
} from "./openclaw-runtime-memory.js";
|
||||
import { CHARS_PER_TOKEN_ESTIMATE, getMemoryHostServices } from "./services.js";
|
||||
|
||||
export { hashText } from "./hash.js";
|
||||
import { hashText } from "./hash.js";
|
||||
@@ -144,7 +135,7 @@ export async function listMemoryFiles(
|
||||
const memoryDir = path.join(workspaceDir, "memory");
|
||||
|
||||
const shouldSkipWorkspaceMemoryPath = (absPath: string): boolean =>
|
||||
shouldSkipRootMemoryAuxiliaryPath({ workspaceDir, absPath });
|
||||
getMemoryHostServices().memory.shouldSkipRootMemoryAuxiliaryPath({ workspaceDir, absPath });
|
||||
|
||||
const addMarkdownFile = async (absPath: string) => {
|
||||
try {
|
||||
@@ -159,7 +150,8 @@ export async function listMemoryFiles(
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const memoryFile = await resolveCanonicalRootMemoryFile(workspaceDir);
|
||||
const memoryFile =
|
||||
await getMemoryHostServices().memory.resolveCanonicalRootMemoryFile(workspaceDir);
|
||||
if (memoryFile) {
|
||||
await addMarkdownFile(memoryFile);
|
||||
}
|
||||
@@ -240,7 +232,10 @@ export async function buildFileEntry(
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const mimeType = await detectMime({ buffer: buffer.subarray(0, 512), filePath: absPath });
|
||||
const mimeType = await getMemoryHostServices().io.detectMime({
|
||||
buffer: buffer.subarray(0, 512),
|
||||
filePath: absPath,
|
||||
});
|
||||
if (!mimeType || !mimeType.startsWith(`${modality}/`)) {
|
||||
return null;
|
||||
}
|
||||
@@ -405,7 +400,7 @@ export function chunkMarkdown(
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
acc += estimateStringChars(entry.line) + 1;
|
||||
acc += getMemoryHostServices().io.estimateStringChars(entry.line) + 1;
|
||||
kept.unshift(entry);
|
||||
if (acc >= overlapChars) {
|
||||
break;
|
||||
@@ -428,7 +423,7 @@ export function chunkMarkdown(
|
||||
// chunking.tokens so the chunk stays within the token budget.
|
||||
for (let start = 0; start < line.length; start += maxChars) {
|
||||
const coarse = line.slice(start, start + maxChars);
|
||||
if (estimateStringChars(coarse) > maxChars) {
|
||||
if (getMemoryHostServices().io.estimateStringChars(coarse) > maxChars) {
|
||||
const fineStep = Math.max(1, chunking.tokens);
|
||||
for (let j = 0; j < coarse.length; ) {
|
||||
let end = Math.min(j + fineStep, coarse.length);
|
||||
@@ -448,7 +443,7 @@ export function chunkMarkdown(
|
||||
}
|
||||
}
|
||||
for (const segment of segments) {
|
||||
const lineSize = estimateStringChars(segment) + 1;
|
||||
const lineSize = getMemoryHostServices().io.estimateStringChars(segment) + 1;
|
||||
if (currentChars + lineSize > maxChars && current.length > 0) {
|
||||
flush();
|
||||
carryOverlap();
|
||||
@@ -516,11 +511,12 @@ export async function runWithConcurrency<T>(
|
||||
tasks: Array<() => Promise<T>>,
|
||||
limit: number,
|
||||
): Promise<T[]> {
|
||||
const { results, firstError, hasError } = await runTasksWithConcurrency({
|
||||
tasks,
|
||||
limit,
|
||||
errorMode: "stop",
|
||||
});
|
||||
const { results, firstError, hasError } =
|
||||
await getMemoryHostServices().io.runTasksWithConcurrency({
|
||||
tasks,
|
||||
limit,
|
||||
errorMode: "stop",
|
||||
});
|
||||
if (hasError) {
|
||||
throw firstError;
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export {
|
||||
DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR,
|
||||
asToolParamsRecord,
|
||||
jsonResult,
|
||||
parseAgentSessionKey,
|
||||
readNumberParam,
|
||||
readStringParam,
|
||||
resolveAgentContextLimits,
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveCronStyleNow,
|
||||
resolveDefaultAgentId,
|
||||
resolveMemorySearchConfig,
|
||||
resolveMemorySearchSyncConfig,
|
||||
resolveSessionAgentId,
|
||||
} from "./openclaw-runtime.js";
|
||||
export type {
|
||||
AnyAgentTool,
|
||||
ResolvedMemorySearchConfig,
|
||||
ResolvedMemorySearchSyncConfig,
|
||||
} from "./openclaw-runtime.js";
|
||||
@@ -1 +0,0 @@
|
||||
export { requireApiKey, resolveApiKeyForProvider } from "./openclaw-runtime.js";
|
||||
@@ -1,17 +0,0 @@
|
||||
export {
|
||||
colorize,
|
||||
defaultRuntime,
|
||||
formatDocsLink,
|
||||
formatErrorMessage,
|
||||
formatHelpExamples,
|
||||
isRich,
|
||||
isVerbose,
|
||||
resolveCommandSecretRefsViaGateway,
|
||||
setVerbose,
|
||||
shortenHomeInString,
|
||||
shortenHomePath,
|
||||
theme,
|
||||
withManager,
|
||||
withProgress,
|
||||
withProgressTotals,
|
||||
} from "./openclaw-runtime.js";
|
||||
@@ -1,22 +0,0 @@
|
||||
export {
|
||||
getRuntimeConfig,
|
||||
hasConfiguredSecretInput,
|
||||
loadConfig,
|
||||
normalizeResolvedSecretInputString,
|
||||
parseDurationMs,
|
||||
parseNonNegativeByteSize,
|
||||
resolveSessionTranscriptsDirForAgent,
|
||||
resolveStateDir,
|
||||
} from "./openclaw-runtime.js";
|
||||
export type {
|
||||
MemoryBackend,
|
||||
MemoryCitationsMode,
|
||||
MemoryQmdConfig,
|
||||
MemoryQmdIndexPath,
|
||||
MemoryQmdMcporterConfig,
|
||||
MemoryQmdSearchMode,
|
||||
MemorySearchConfig,
|
||||
OpenClawConfig,
|
||||
SecretInput,
|
||||
SessionSendPolicyConfig,
|
||||
} from "./openclaw-runtime.js";
|
||||
@@ -1,15 +0,0 @@
|
||||
export {
|
||||
CHARS_PER_TOKEN_ESTIMATE,
|
||||
createSubsystemLogger,
|
||||
detectMime,
|
||||
estimateStringChars,
|
||||
redactSensitiveText,
|
||||
resolveGlobalSingleton,
|
||||
resolveUserPath,
|
||||
runTasksWithConcurrency,
|
||||
shortenHomeInString,
|
||||
shortenHomePath,
|
||||
splitShellArgs,
|
||||
truncateUtf16Safe,
|
||||
writeFileWithinRoot,
|
||||
} from "./openclaw-runtime.js";
|
||||
@@ -1,29 +0,0 @@
|
||||
export {
|
||||
buildActiveMemoryPromptSection,
|
||||
emptyPluginConfigSchema,
|
||||
getMemoryCapabilityRegistration,
|
||||
getMemoryEmbeddingProvider,
|
||||
listActiveMemoryPublicArtifacts,
|
||||
listMemoryEmbeddingProviders,
|
||||
listRegisteredMemoryEmbeddingProviderAdapters,
|
||||
listRegisteredMemoryEmbeddingProviders,
|
||||
resolveCanonicalRootMemoryFile,
|
||||
shouldSkipRootMemoryAuxiliaryPath,
|
||||
} from "./openclaw-runtime.js";
|
||||
export type {
|
||||
MemoryEmbeddingBatchChunk,
|
||||
MemoryEmbeddingBatchOptions,
|
||||
MemoryEmbeddingProvider,
|
||||
MemoryEmbeddingProviderAdapter,
|
||||
MemoryEmbeddingProviderCreateOptions,
|
||||
MemoryEmbeddingProviderCreateResult,
|
||||
MemoryEmbeddingProviderRuntime,
|
||||
MemoryFlushPlan,
|
||||
MemoryFlushPlanResolver,
|
||||
MemoryPluginCapability,
|
||||
MemoryPluginPublicArtifact,
|
||||
MemoryPluginPublicArtifactsProvider,
|
||||
MemoryPluginRuntime,
|
||||
MemoryPromptSectionBuilder,
|
||||
OpenClawPluginApi,
|
||||
} from "./openclaw-runtime.js";
|
||||
@@ -1,5 +0,0 @@
|
||||
export {
|
||||
fetchWithSsrFGuard,
|
||||
shouldUseEnvHttpProxyForUrl,
|
||||
ssrfPolicyFromHttpBaseUrlAllowedHostname,
|
||||
} from "./openclaw-runtime.js";
|
||||
@@ -1,18 +0,0 @@
|
||||
export {
|
||||
HEARTBEAT_PROMPT,
|
||||
HEARTBEAT_TOKEN,
|
||||
SILENT_REPLY_TOKEN,
|
||||
hasInterSessionUserProvenance,
|
||||
isCompactionCheckpointTranscriptFileName,
|
||||
isCronRunSessionKey,
|
||||
isExecCompletionEvent,
|
||||
isHeartbeatUserMessage,
|
||||
isSessionArchiveArtifactName,
|
||||
isSilentReplyPayloadText,
|
||||
isUsageCountedSessionTranscriptFileName,
|
||||
onSessionTranscriptUpdate,
|
||||
parseUsageCountedSessionIdFromFileName,
|
||||
resolveSessionTranscriptsDirForAgent,
|
||||
stripInboundMetadata,
|
||||
stripInternalRuntimeContext,
|
||||
} from "./openclaw-runtime.js";
|
||||
@@ -1,139 +0,0 @@
|
||||
// Agent/runtime helpers.
|
||||
export { resolveCronStyleNow } from "../../../../src/agents/current-time.js";
|
||||
export {
|
||||
resolveAgentContextLimits,
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionAgentId,
|
||||
} from "../../../../src/agents/agent-scope.js";
|
||||
export { requireApiKey, resolveApiKeyForProvider } from "../../../../src/agents/model-auth.js";
|
||||
export { stripInternalRuntimeContext } from "../../../../src/agents/internal-runtime-context.js";
|
||||
export { DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR } from "../../../../src/agents/pi-settings.js";
|
||||
export {
|
||||
asToolParamsRecord,
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readStringParam,
|
||||
} from "../../../../src/agents/tools/common.js";
|
||||
export type { AnyAgentTool } from "../../../../src/agents/tools/common.js";
|
||||
export {
|
||||
resolveMemorySearchConfig,
|
||||
resolveMemorySearchSyncConfig,
|
||||
type ResolvedMemorySearchConfig,
|
||||
type ResolvedMemorySearchSyncConfig,
|
||||
} from "../../../../src/agents/memory-search.js";
|
||||
|
||||
// Session and reply helpers.
|
||||
export { isHeartbeatUserMessage } from "../../../../src/auto-reply/heartbeat-filter.js";
|
||||
export { HEARTBEAT_PROMPT } from "../../../../src/auto-reply/heartbeat.js";
|
||||
export { stripInboundMetadata } from "../../../../src/auto-reply/reply/strip-inbound-meta.js";
|
||||
export {
|
||||
HEARTBEAT_TOKEN,
|
||||
SILENT_REPLY_TOKEN,
|
||||
isSilentReplyPayloadText,
|
||||
} from "../../../../src/auto-reply/tokens.js";
|
||||
|
||||
// CLI/runtime/config helpers.
|
||||
export { formatErrorMessage, withManager } from "../../../../src/cli/cli-utils.js";
|
||||
export { resolveCommandSecretRefsViaGateway } from "../../../../src/cli/command-secret-gateway.js";
|
||||
export { formatHelpExamples } from "../../../../src/cli/help-format.js";
|
||||
export { parseDurationMs } from "../../../../src/cli/parse-duration.js";
|
||||
export { withProgress, withProgressTotals } from "../../../../src/cli/progress.js";
|
||||
export { parseNonNegativeByteSize } from "../../../../src/config/byte-size.js";
|
||||
export {
|
||||
getRuntimeConfig,
|
||||
/** @deprecated Use getRuntimeConfig(), or pass the already loaded config through the call path. */
|
||||
loadConfig,
|
||||
} from "../../../../src/config/config.js";
|
||||
export type { OpenClawConfig } from "../../../../src/config/config.js";
|
||||
export { resolveStateDir } from "../../../../src/config/paths.js";
|
||||
export {
|
||||
isCompactionCheckpointTranscriptFileName,
|
||||
isSessionArchiveArtifactName,
|
||||
isUsageCountedSessionTranscriptFileName,
|
||||
parseUsageCountedSessionIdFromFileName,
|
||||
} from "../../../../src/config/sessions/artifacts.js";
|
||||
export { resolveSessionTranscriptsDirForAgent } from "../../../../src/config/sessions/paths.js";
|
||||
export type { SessionSendPolicyConfig } from "../../../../src/config/types.base.js";
|
||||
export type {
|
||||
MemoryBackend,
|
||||
MemoryCitationsMode,
|
||||
MemoryQmdConfig,
|
||||
MemoryQmdIndexPath,
|
||||
MemoryQmdMcporterConfig,
|
||||
MemoryQmdSearchMode,
|
||||
} from "../../../../src/config/types.memory.js";
|
||||
export {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
} from "../../../../src/config/types.secrets.js";
|
||||
export type { SecretInput } from "../../../../src/config/types.secrets.js";
|
||||
export type { MemorySearchConfig } from "../../../../src/config/types.tools.js";
|
||||
export { isVerbose, setVerbose } from "../../../../src/globals.js";
|
||||
|
||||
// IO, network, and logging helpers.
|
||||
export { isExecCompletionEvent } from "../../../../src/infra/heartbeat-events-filter.js";
|
||||
export { writeFileWithinRoot } from "../../../../src/infra/fs-safe.js";
|
||||
export { fetchWithSsrFGuard } from "../../../../src/infra/net/fetch-guard.js";
|
||||
export { shouldUseEnvHttpProxyForUrl } from "../../../../src/infra/net/proxy-env.js";
|
||||
export { ssrfPolicyFromHttpBaseUrlAllowedHostname } from "../../../../src/infra/net/ssrf.js";
|
||||
export { redactSensitiveText } from "../../../../src/logging/redact.js";
|
||||
export { createSubsystemLogger } from "../../../../src/logging/subsystem.js";
|
||||
export { detectMime } from "../../../../src/media/mime.js";
|
||||
|
||||
// Memory plugin helpers.
|
||||
export {
|
||||
resolveCanonicalRootMemoryFile,
|
||||
shouldSkipRootMemoryAuxiliaryPath,
|
||||
} from "../../../../src/memory/root-memory-files.js";
|
||||
export {
|
||||
getMemoryEmbeddingProvider,
|
||||
listMemoryEmbeddingProviders,
|
||||
listRegisteredMemoryEmbeddingProviderAdapters,
|
||||
listRegisteredMemoryEmbeddingProviders,
|
||||
} from "../../../../src/plugins/memory-embedding-provider-runtime.js";
|
||||
export type {
|
||||
MemoryEmbeddingBatchChunk,
|
||||
MemoryEmbeddingBatchOptions,
|
||||
MemoryEmbeddingProvider,
|
||||
MemoryEmbeddingProviderAdapter,
|
||||
MemoryEmbeddingProviderCreateOptions,
|
||||
MemoryEmbeddingProviderCreateResult,
|
||||
MemoryEmbeddingProviderRuntime,
|
||||
} from "../../../../src/plugins/memory-embedding-providers.js";
|
||||
export { emptyPluginConfigSchema } from "../../../../src/plugins/config-schema.js";
|
||||
export {
|
||||
buildMemoryPromptSection as buildActiveMemoryPromptSection,
|
||||
getMemoryCapabilityRegistration,
|
||||
listActiveMemoryPublicArtifacts,
|
||||
} from "../../../../src/plugins/memory-state.js";
|
||||
export type {
|
||||
MemoryFlushPlan,
|
||||
MemoryFlushPlanResolver,
|
||||
MemoryPluginCapability,
|
||||
MemoryPluginPublicArtifact,
|
||||
MemoryPluginPublicArtifactsProvider,
|
||||
MemoryPluginRuntime,
|
||||
MemoryPromptSectionBuilder,
|
||||
} from "../../../../src/plugins/memory-state.js";
|
||||
export type { OpenClawPluginApi } from "../../../../src/plugins/types.js";
|
||||
|
||||
// Shared session/text utilities.
|
||||
export { defaultRuntime } from "../../../../src/runtime.js";
|
||||
export { parseAgentSessionKey } from "../../../../src/routing/session-key.js";
|
||||
export { hasInterSessionUserProvenance } from "../../../../src/sessions/input-provenance.js";
|
||||
export { isCronRunSessionKey } from "../../../../src/sessions/session-key-utils.js";
|
||||
export { onSessionTranscriptUpdate } from "../../../../src/sessions/transcript-events.js";
|
||||
export { formatDocsLink } from "../../../../src/terminal/links.js";
|
||||
export { colorize, isRich, theme } from "../../../../src/terminal/theme.js";
|
||||
export { CHARS_PER_TOKEN_ESTIMATE, estimateStringChars } from "../../../../src/utils/cjk-chars.js";
|
||||
export { runTasksWithConcurrency } from "../../../../src/utils/run-with-concurrency.js";
|
||||
export { splitShellArgs } from "../../../../src/utils/shell-argv.js";
|
||||
export {
|
||||
resolveUserPath,
|
||||
shortenHomeInString,
|
||||
shortenHomePath,
|
||||
truncateUtf16Safe,
|
||||
} from "../../../../src/utils.js";
|
||||
export { resolveGlobalSingleton } from "../../../../src/shared/global-singleton.js";
|
||||
@@ -1,28 +1,31 @@
|
||||
import {
|
||||
fetchWithSsrFGuard,
|
||||
shouldUseEnvHttpProxyForUrl,
|
||||
ssrfPolicyFromHttpBaseUrlAllowedHostname,
|
||||
} from "./openclaw-runtime-network.js";
|
||||
getMemoryHostServices,
|
||||
MEMORY_REMOTE_TRUSTED_ENV_PROXY_MODE,
|
||||
type MemoryHostGuardedFetch,
|
||||
} from "./services.js";
|
||||
import type { SsrFPolicy } from "./ssrf-policy.js";
|
||||
|
||||
export const MEMORY_REMOTE_TRUSTED_ENV_PROXY_MODE = "trusted_env_proxy";
|
||||
export { MEMORY_REMOTE_TRUSTED_ENV_PROXY_MODE };
|
||||
|
||||
export const buildRemoteBaseUrlPolicy: (baseUrl: string) => SsrFPolicy | undefined =
|
||||
ssrfPolicyFromHttpBaseUrlAllowedHostname;
|
||||
export const buildRemoteBaseUrlPolicy: (baseUrl: string) => SsrFPolicy | undefined = (baseUrl) =>
|
||||
getMemoryHostServices().network.buildRemoteBaseUrlPolicy(baseUrl);
|
||||
|
||||
export async function withRemoteHttpResponse<T>(params: {
|
||||
url: string;
|
||||
init?: RequestInit;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
fetchImpl?: typeof fetch;
|
||||
fetchWithSsrFGuardImpl?: typeof fetchWithSsrFGuard;
|
||||
shouldUseEnvHttpProxyForUrlImpl?: typeof shouldUseEnvHttpProxyForUrl;
|
||||
fetchWithSsrFGuardImpl?: MemoryHostGuardedFetch;
|
||||
shouldUseEnvHttpProxyForUrlImpl?: (url: string) => boolean;
|
||||
auditContext?: string;
|
||||
onResponse: (response: Response) => Promise<T>;
|
||||
}): Promise<T> {
|
||||
const guardedFetch = params.fetchWithSsrFGuardImpl ?? fetchWithSsrFGuard;
|
||||
const shouldUseEnvProxy = params.shouldUseEnvHttpProxyForUrlImpl ?? shouldUseEnvHttpProxyForUrl;
|
||||
const { response, release } = await guardedFetch({
|
||||
const services = getMemoryHostServices().network;
|
||||
const guardedFetch = params.fetchWithSsrFGuardImpl ?? services.fetchWithSsrFGuard;
|
||||
const shouldUseEnvProxy =
|
||||
params.shouldUseEnvHttpProxyForUrlImpl ??
|
||||
((url: string) => services.shouldUseEnvHttpProxyForUrl(url));
|
||||
const guardedResponse = await guardedFetch({
|
||||
url: params.url,
|
||||
fetchImpl: params.fetchImpl,
|
||||
init: params.init,
|
||||
@@ -31,8 +34,8 @@ export async function withRemoteHttpResponse<T>(params: {
|
||||
...(shouldUseEnvProxy(params.url) ? { mode: MEMORY_REMOTE_TRUSTED_ENV_PROXY_MODE } : {}),
|
||||
});
|
||||
try {
|
||||
return await params.onResponse(response);
|
||||
return await params.onResponse(guardedResponse.response);
|
||||
} finally {
|
||||
await release();
|
||||
await guardedResponse.release();
|
||||
}
|
||||
}
|
||||
|
||||
77
packages/memory-host-sdk/src/host/services.test.ts
Normal file
77
packages/memory-host-sdk/src/host/services.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createDefaultMemoryHostServices,
|
||||
getMemoryHostServices,
|
||||
setMemoryHostServices,
|
||||
withMemoryHostServices,
|
||||
type MemoryHostServices,
|
||||
} from "./services.js";
|
||||
|
||||
function makeServices(charCount: number): MemoryHostServices {
|
||||
const services = createDefaultMemoryHostServices();
|
||||
return {
|
||||
...services,
|
||||
io: {
|
||||
...services.io,
|
||||
estimateStringChars: () => charCount,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("MemoryHostServices", () => {
|
||||
it("returns a restore callback when overriding services", () => {
|
||||
const baseline = createDefaultMemoryHostServices();
|
||||
const restoreBaseline = setMemoryHostServices(baseline);
|
||||
try {
|
||||
const first = makeServices(11);
|
||||
const second = makeServices(22);
|
||||
|
||||
const restoreFirst = setMemoryHostServices(first);
|
||||
expect(getMemoryHostServices().io.estimateStringChars("abc")).toBe(11);
|
||||
|
||||
const restoreSecond = setMemoryHostServices(second);
|
||||
expect(getMemoryHostServices().io.estimateStringChars("abc")).toBe(22);
|
||||
|
||||
restoreSecond();
|
||||
expect(getMemoryHostServices()).toBe(first);
|
||||
expect(getMemoryHostServices().io.estimateStringChars("abc")).toBe(11);
|
||||
|
||||
restoreFirst();
|
||||
expect(getMemoryHostServices()).toBe(baseline);
|
||||
} finally {
|
||||
restoreBaseline();
|
||||
}
|
||||
});
|
||||
|
||||
it("scopes service overrides across async failures", async () => {
|
||||
const baseline = createDefaultMemoryHostServices();
|
||||
const restoreBaseline = setMemoryHostServices(baseline);
|
||||
try {
|
||||
await expect(
|
||||
withMemoryHostServices(makeServices(33), async () => {
|
||||
expect(getMemoryHostServices().io.estimateStringChars("abc")).toBe(33);
|
||||
throw new Error("boom");
|
||||
}),
|
||||
).rejects.toThrow("boom");
|
||||
|
||||
expect(getMemoryHostServices()).toBe(baseline);
|
||||
} finally {
|
||||
restoreBaseline();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps host-owned network fetch fail-closed by default", async () => {
|
||||
const services = createDefaultMemoryHostServices();
|
||||
|
||||
await expect(
|
||||
services.network.fetchWithSsrFGuard({ url: "https://memory.example/v1" }),
|
||||
).rejects.toThrow("requires a host service binding");
|
||||
});
|
||||
|
||||
it("redacts likely secrets in the package default service", () => {
|
||||
const services = createDefaultMemoryHostServices();
|
||||
const secret = "OPENAI_API_KEY=sk-1234567890abcdef";
|
||||
|
||||
expect(services.io.redactSensitiveText(secret)).not.toContain("sk-1234567890abcdef");
|
||||
});
|
||||
});
|
||||
713
packages/memory-host-sdk/src/host/services.ts
Normal file
713
packages/memory-host-sdk/src/host/services.ts
Normal file
@@ -0,0 +1,713 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { normalizeAgentId, resolveStateDir } from "./config-utils.js";
|
||||
import type { SsrFPolicy } from "./ssrf-policy.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
|
||||
export const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
|
||||
export const SILENT_REPLY_TOKEN = "NO_REPLY";
|
||||
export const HEARTBEAT_PROMPT =
|
||||
"Run the following periodic tasks (only those due based on their intervals):";
|
||||
export const CHARS_PER_TOKEN_ESTIMATE = 4;
|
||||
export const MEMORY_REMOTE_TRUSTED_ENV_PROXY_MODE = "trusted_env_proxy";
|
||||
|
||||
export type MemoryHostLogger = {
|
||||
debug(message: string): void;
|
||||
};
|
||||
|
||||
export type MemoryHostGuardedFetch = (params: {
|
||||
url: string;
|
||||
fetchImpl?: typeof fetch;
|
||||
init?: RequestInit;
|
||||
policy?: SsrFPolicy;
|
||||
auditContext?: string;
|
||||
mode?: string;
|
||||
}) => Promise<{ response: Response; release(): Promise<void> }>;
|
||||
|
||||
export type MemoryHostServices = {
|
||||
auth: {
|
||||
requireApiKey(apiKey: string | undefined, provider: string): string;
|
||||
resolveApiKeyForProvider(params: {
|
||||
provider: string;
|
||||
cfg: unknown;
|
||||
agentDir?: string;
|
||||
}): Promise<string | undefined>;
|
||||
};
|
||||
io: {
|
||||
createSubsystemLogger(name: string): MemoryHostLogger;
|
||||
detectMime(opts: {
|
||||
buffer?: Buffer;
|
||||
headerMime?: string | null;
|
||||
filePath?: string;
|
||||
}): Promise<string | undefined>;
|
||||
estimateStringChars(text: string): number;
|
||||
redactSensitiveText(text: string, options?: unknown): string;
|
||||
runTasksWithConcurrency<T>(params: {
|
||||
tasks: Array<() => Promise<T>>;
|
||||
limit: number;
|
||||
errorMode?: "continue" | "stop";
|
||||
onTaskError?: (error: unknown, index: number) => void;
|
||||
}): Promise<{ results: T[]; firstError: unknown; hasError: boolean }>;
|
||||
};
|
||||
memory: {
|
||||
resolveCanonicalRootMemoryFile(workspaceDir: string): Promise<string | null>;
|
||||
shouldSkipRootMemoryAuxiliaryPath(params: { workspaceDir: string; absPath: string }): boolean;
|
||||
};
|
||||
network: {
|
||||
buildRemoteBaseUrlPolicy(baseUrl: string): SsrFPolicy | undefined;
|
||||
fetchWithSsrFGuard: MemoryHostGuardedFetch;
|
||||
shouldUseEnvHttpProxyForUrl(url: string): boolean;
|
||||
};
|
||||
session: {
|
||||
hasInterSessionUserProvenance(
|
||||
message: { role?: unknown; provenance?: unknown } | undefined,
|
||||
): boolean;
|
||||
isCompactionCheckpointTranscriptFileName(fileName: string): boolean;
|
||||
isCronRunSessionKey(sessionKey: string | undefined | null): boolean;
|
||||
isExecCompletionEvent(event: string): boolean;
|
||||
isHeartbeatUserMessage(
|
||||
message: { role: string; content?: unknown },
|
||||
heartbeatPrompt?: string,
|
||||
): boolean;
|
||||
isSessionArchiveArtifactName(fileName: string): boolean;
|
||||
isSilentReplyPayloadText(text: string | undefined): boolean;
|
||||
isUsageCountedSessionTranscriptFileName(fileName: string): boolean;
|
||||
parseUsageCountedSessionIdFromFileName(fileName: string): string | null;
|
||||
resolveSessionTranscriptsDirForAgent(agentId: string): string;
|
||||
stripInboundMetadata(text: string): string;
|
||||
stripInternalRuntimeContext(text: string): string;
|
||||
};
|
||||
};
|
||||
|
||||
let activeServices: MemoryHostServices | undefined;
|
||||
|
||||
export function setMemoryHostServices(services: MemoryHostServices): () => void {
|
||||
const previousServices = activeServices;
|
||||
activeServices = services;
|
||||
let restored = false;
|
||||
return () => {
|
||||
if (restored) {
|
||||
return;
|
||||
}
|
||||
restored = true;
|
||||
activeServices = previousServices;
|
||||
};
|
||||
}
|
||||
|
||||
export async function withMemoryHostServices<T>(
|
||||
services: MemoryHostServices,
|
||||
run: () => T | Promise<T>,
|
||||
): Promise<Awaited<T>> {
|
||||
const restore = setMemoryHostServices(services);
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
}
|
||||
|
||||
export function getMemoryHostServices(): MemoryHostServices {
|
||||
activeServices ??= createDefaultMemoryHostServices();
|
||||
return activeServices;
|
||||
}
|
||||
|
||||
export function createDefaultMemoryHostServices(): MemoryHostServices {
|
||||
return {
|
||||
auth: {
|
||||
requireApiKey(apiKey, provider) {
|
||||
const trimmed = apiKey?.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(`${provider} API key required`);
|
||||
}
|
||||
return trimmed;
|
||||
},
|
||||
async resolveApiKeyForProvider() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
io: {
|
||||
createSubsystemLogger: () => ({ debug: () => {} }),
|
||||
detectMime: async ({ filePath }) => mimeTypeFromFilePath(filePath),
|
||||
estimateStringChars,
|
||||
redactSensitiveText,
|
||||
runTasksWithConcurrency,
|
||||
},
|
||||
memory: {
|
||||
resolveCanonicalRootMemoryFile,
|
||||
shouldSkipRootMemoryAuxiliaryPath,
|
||||
},
|
||||
network: {
|
||||
buildRemoteBaseUrlPolicy,
|
||||
async fetchWithSsrFGuard() {
|
||||
throw new Error(
|
||||
"@openclaw/memory-host-sdk network.fetchWithSsrFGuard requires a host service binding",
|
||||
);
|
||||
},
|
||||
shouldUseEnvHttpProxyForUrl: () => false,
|
||||
},
|
||||
session: {
|
||||
hasInterSessionUserProvenance,
|
||||
isCompactionCheckpointTranscriptFileName,
|
||||
isCronRunSessionKey,
|
||||
isExecCompletionEvent,
|
||||
isHeartbeatUserMessage,
|
||||
isSessionArchiveArtifactName,
|
||||
isSilentReplyPayloadText,
|
||||
isUsageCountedSessionTranscriptFileName,
|
||||
parseUsageCountedSessionIdFromFileName,
|
||||
resolveSessionTranscriptsDirForAgent,
|
||||
stripInboundMetadata,
|
||||
stripInternalRuntimeContext,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ROOT_MEMORY_REPAIR_RELATIVE_DIR = ".openclaw-repair/root-memory";
|
||||
const LEGACY_ROOT_MEMORY_FILENAME = "memory.md";
|
||||
const ARCHIVE_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(?:\.\d{3})?Z$/;
|
||||
const LEGACY_STORE_BACKUP_RE = /^sessions\.json\.bak\.\d+$/;
|
||||
const COMPACTION_CHECKPOINT_TRANSCRIPT_RE =
|
||||
/^(.+)\.checkpoint\.([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.jsonl$/i;
|
||||
|
||||
async function resolveCanonicalRootMemoryFile(workspaceDir: string): Promise<string | null> {
|
||||
try {
|
||||
const entries = await fs.readdir(workspaceDir, { withFileTypes: true });
|
||||
const entry = entries.find(
|
||||
(candidate) =>
|
||||
candidate.name === "MEMORY.md" && candidate.isFile() && !candidate.isSymbolicLink(),
|
||||
);
|
||||
return entry ? path.join(workspaceDir, entry.name) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSkipRootMemoryAuxiliaryPath(params: {
|
||||
workspaceDir: string;
|
||||
absPath: string;
|
||||
}): boolean {
|
||||
const relative = path.relative(params.workspaceDir, params.absPath);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = relative.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
||||
return (
|
||||
normalized === LEGACY_ROOT_MEMORY_FILENAME ||
|
||||
normalized === ROOT_MEMORY_REPAIR_RELATIVE_DIR ||
|
||||
normalized.startsWith(`${ROOT_MEMORY_REPAIR_RELATIVE_DIR}/`)
|
||||
);
|
||||
}
|
||||
|
||||
function hasArchiveSuffix(fileName: string, reason: "bak" | "reset" | "deleted"): boolean {
|
||||
const marker = `.${reason}.`;
|
||||
const index = fileName.lastIndexOf(marker);
|
||||
return index >= 0 && ARCHIVE_TIMESTAMP_RE.test(fileName.slice(index + marker.length));
|
||||
}
|
||||
|
||||
function isSessionArchiveArtifactName(fileName: string): boolean {
|
||||
if (LEGACY_STORE_BACKUP_RE.test(fileName)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
hasArchiveSuffix(fileName, "deleted") ||
|
||||
hasArchiveSuffix(fileName, "reset") ||
|
||||
hasArchiveSuffix(fileName, "bak")
|
||||
);
|
||||
}
|
||||
|
||||
function isCompactionCheckpointTranscriptFileName(fileName: string): boolean {
|
||||
return COMPACTION_CHECKPOINT_TRANSCRIPT_RE.test(fileName);
|
||||
}
|
||||
|
||||
function isPrimarySessionTranscriptFileName(fileName: string): boolean {
|
||||
return (
|
||||
fileName !== "sessions.json" &&
|
||||
fileName.endsWith(".jsonl") &&
|
||||
!fileName.endsWith(".trajectory.jsonl") &&
|
||||
!isCompactionCheckpointTranscriptFileName(fileName) &&
|
||||
!isSessionArchiveArtifactName(fileName)
|
||||
);
|
||||
}
|
||||
|
||||
function isUsageCountedSessionTranscriptFileName(fileName: string): boolean {
|
||||
return (
|
||||
isPrimarySessionTranscriptFileName(fileName) ||
|
||||
hasArchiveSuffix(fileName, "reset") ||
|
||||
hasArchiveSuffix(fileName, "deleted")
|
||||
);
|
||||
}
|
||||
|
||||
function parseUsageCountedSessionIdFromFileName(fileName: string): string | null {
|
||||
if (isPrimarySessionTranscriptFileName(fileName)) {
|
||||
return fileName.slice(0, -".jsonl".length);
|
||||
}
|
||||
for (const reason of ["reset", "deleted"] as const) {
|
||||
const marker = `.jsonl.${reason}.`;
|
||||
const index = fileName.lastIndexOf(marker);
|
||||
if (index > 0 && hasArchiveSuffix(fileName, reason)) {
|
||||
return fileName.slice(0, index);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveSessionTranscriptsDirForAgent(agentId: string): string {
|
||||
return path.join(resolveStateDir(), "agents", normalizeAgentId(agentId), "sessions");
|
||||
}
|
||||
|
||||
function parseAgentSessionKey(sessionKey: string | undefined | null): { rest: string } | null {
|
||||
const raw = normalizeLowercaseStringOrEmpty(sessionKey ?? "");
|
||||
const parts = raw.split(":").filter(Boolean);
|
||||
if (parts.length < 3 || parts[0] !== "agent" || !parts[1]) {
|
||||
return null;
|
||||
}
|
||||
const rest = parts.slice(2).join(":");
|
||||
return rest ? { rest } : null;
|
||||
}
|
||||
|
||||
function isCronRunSessionKey(sessionKey: string | undefined | null): boolean {
|
||||
const parsed = parseAgentSessionKey(sessionKey);
|
||||
return parsed ? /^cron:[^:]+:run:[^:]+$/.test(parsed.rest) : false;
|
||||
}
|
||||
|
||||
function isExecCompletionEvent(event: string): boolean {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(event).trimStart();
|
||||
return (
|
||||
/^exec finished(?::|\s*\()/.test(normalized) ||
|
||||
/^exec (completed|failed) \([a-z0-9_-]{1,64}, (code -?\d+|signal [^)]+)\)( :: .*)?$/.test(
|
||||
normalized,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isSilentReplyPayloadText(text: string | undefined): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const trimmed = text.trim();
|
||||
if (new RegExp(`^${escapeRegExp(SILENT_REPLY_TOKEN)}$`, "i").test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}") || !trimmed.includes(SILENT_REPLY_TOKEN)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as { action?: unknown };
|
||||
return (
|
||||
Object.keys(parsed).length === 1 &&
|
||||
typeof parsed.action === "string" &&
|
||||
parsed.action.trim() === SILENT_REPLY_TOKEN
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveMessageText(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
if (!Array.isArray(content)) {
|
||||
return "";
|
||||
}
|
||||
return content
|
||||
.filter(
|
||||
(block): block is { type: "text"; text: string } =>
|
||||
Boolean(block) &&
|
||||
typeof block === "object" &&
|
||||
(block as { type?: unknown }).type === "text" &&
|
||||
typeof (block as { text?: unknown }).text === "string",
|
||||
)
|
||||
.map((block) => block.text)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function isHeartbeatUserMessage(
|
||||
message: { role: string; content?: unknown },
|
||||
heartbeatPrompt?: string,
|
||||
): boolean {
|
||||
if (message.role !== "user") {
|
||||
return false;
|
||||
}
|
||||
const trimmed = resolveMessageText(message.content).trim();
|
||||
return Boolean(
|
||||
trimmed &&
|
||||
((heartbeatPrompt?.trim() && trimmed.startsWith(heartbeatPrompt.trim())) ||
|
||||
(trimmed.startsWith(HEARTBEAT_PROMPT) && trimmed.includes("HEARTBEAT_OK"))),
|
||||
);
|
||||
}
|
||||
|
||||
function hasInterSessionUserProvenance(
|
||||
message: { role?: unknown; provenance?: unknown } | undefined,
|
||||
): boolean {
|
||||
return (
|
||||
message?.role === "user" &&
|
||||
Boolean(message.provenance) &&
|
||||
typeof message.provenance === "object" &&
|
||||
(message.provenance as { kind?: unknown }).kind === "inter_session"
|
||||
);
|
||||
}
|
||||
|
||||
const LEADING_TIMESTAMP_PREFIX_RE = /^\[[A-Za-z]{3} \d{4}-\d{2}-\d{2} \d{2}:\d{2}[^\]]*\] */;
|
||||
const INTERNAL_RUNTIME_CONTEXT_BEGIN = "<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>";
|
||||
const INTERNAL_RUNTIME_CONTEXT_END = "<<<END_OPENCLAW_INTERNAL_CONTEXT>>>";
|
||||
const OPENCLAW_RUNTIME_CONTEXT_NOTICE =
|
||||
"This context is runtime-generated, not user-authored. Keep internal details private.";
|
||||
const OPENCLAW_NEXT_TURN_RUNTIME_CONTEXT_HEADER =
|
||||
"OpenClaw runtime context for the immediately preceding user message.";
|
||||
const OPENCLAW_RUNTIME_EVENT_HEADER = "OpenClaw runtime event.";
|
||||
const LEGACY_INTERNAL_CONTEXT_HEADER =
|
||||
["OpenClaw runtime context (internal):", OPENCLAW_RUNTIME_CONTEXT_NOTICE, ""].join("\n") + "\n";
|
||||
const LEGACY_INTERNAL_EVENT_MARKER = "[Internal task completion event]";
|
||||
const LEGACY_INTERNAL_EVENT_SEPARATOR = "\n\n---\n\n";
|
||||
const LEGACY_UNTRUSTED_RESULT_BEGIN = "<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>";
|
||||
const LEGACY_UNTRUSTED_RESULT_END = "<<<END_UNTRUSTED_CHILD_RESULT>>>";
|
||||
const INBOUND_META_SENTINELS = [
|
||||
"Conversation info (untrusted metadata):",
|
||||
"Sender (untrusted metadata):",
|
||||
"Thread starter (untrusted, for context):",
|
||||
"Replied message (untrusted, for context):",
|
||||
"Forwarded message context (untrusted metadata):",
|
||||
"Chat history since last reply (untrusted, for context):",
|
||||
] as const;
|
||||
|
||||
function stripInboundMetadata(text: string): string {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
const withoutTimestamp = text.replace(LEADING_TIMESTAMP_PREFIX_RE, "");
|
||||
if (!INBOUND_META_SENTINELS.some((sentinel) => withoutTimestamp.includes(sentinel))) {
|
||||
return withoutTimestamp;
|
||||
}
|
||||
const lines = withoutTimestamp.split("\n");
|
||||
const result: string[] = [];
|
||||
let inMetaBlock = false;
|
||||
let inJsonFence = false;
|
||||
for (const line of lines) {
|
||||
if (!inMetaBlock && INBOUND_META_SENTINELS.some((sentinel) => line.trim() === sentinel)) {
|
||||
inMetaBlock = true;
|
||||
inJsonFence = false;
|
||||
continue;
|
||||
}
|
||||
if (inMetaBlock) {
|
||||
if (!inJsonFence && line.trim() === "```json") {
|
||||
inJsonFence = true;
|
||||
continue;
|
||||
}
|
||||
if (inJsonFence) {
|
||||
if (line.trim() === "```") {
|
||||
inMetaBlock = false;
|
||||
inJsonFence = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.trim() === "") {
|
||||
continue;
|
||||
}
|
||||
inMetaBlock = false;
|
||||
}
|
||||
result.push(line);
|
||||
}
|
||||
return result.join("\n").replace(/^\n+/, "").replace(/\n+$/, "");
|
||||
}
|
||||
|
||||
function findDelimitedTokenIndex(text: string, token: string, from: number): number {
|
||||
const tokenRe = new RegExp(`(?:^|\\r?\\n)${escapeRegExp(token)}(?=\\r?\\n|$)`, "g");
|
||||
tokenRe.lastIndex = Math.max(0, from);
|
||||
const match = tokenRe.exec(text);
|
||||
if (!match) {
|
||||
return -1;
|
||||
}
|
||||
const prefixLength = match[0].length - token.length;
|
||||
return match.index + prefixLength;
|
||||
}
|
||||
|
||||
function stripDelimitedBlock(text: string, begin: string, end: string): string {
|
||||
let next = text;
|
||||
for (;;) {
|
||||
const start = findDelimitedTokenIndex(next, begin, 0);
|
||||
if (start === -1) {
|
||||
return next;
|
||||
}
|
||||
|
||||
let cursor = start + begin.length;
|
||||
let depth = 1;
|
||||
let finish = -1;
|
||||
while (depth > 0) {
|
||||
const nextBegin = findDelimitedTokenIndex(next, begin, cursor);
|
||||
const nextEnd = findDelimitedTokenIndex(next, end, cursor);
|
||||
if (nextEnd === -1) {
|
||||
break;
|
||||
}
|
||||
if (nextBegin !== -1 && nextBegin < nextEnd) {
|
||||
depth += 1;
|
||||
cursor = nextBegin + begin.length;
|
||||
continue;
|
||||
}
|
||||
depth -= 1;
|
||||
finish = nextEnd;
|
||||
cursor = nextEnd + end.length;
|
||||
}
|
||||
|
||||
const before = next.slice(0, start).trimEnd();
|
||||
if (finish === -1 || depth !== 0) {
|
||||
return before;
|
||||
}
|
||||
const after = next.slice(finish + end.length).trimStart();
|
||||
next = before && after ? `${before}\n\n${after}` : `${before}${after}`;
|
||||
}
|
||||
}
|
||||
|
||||
function findLegacyInternalEventEnd(text: string, start: number): number | null {
|
||||
if (!text.startsWith(LEGACY_INTERNAL_EVENT_MARKER, start)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resultBegin = text.indexOf(
|
||||
LEGACY_UNTRUSTED_RESULT_BEGIN,
|
||||
start + LEGACY_INTERNAL_EVENT_MARKER.length,
|
||||
);
|
||||
if (resultBegin === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resultEnd = text.indexOf(
|
||||
LEGACY_UNTRUSTED_RESULT_END,
|
||||
resultBegin + LEGACY_UNTRUSTED_RESULT_BEGIN.length,
|
||||
);
|
||||
if (resultEnd === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const actionIndex = text.indexOf("\n\nAction:\n", resultEnd + LEGACY_UNTRUSTED_RESULT_END.length);
|
||||
if (actionIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const afterAction = actionIndex + "\n\nAction:\n".length;
|
||||
const nextEvent = text.indexOf(
|
||||
`${LEGACY_INTERNAL_EVENT_SEPARATOR}${LEGACY_INTERNAL_EVENT_MARKER}`,
|
||||
afterAction,
|
||||
);
|
||||
if (nextEvent !== -1) {
|
||||
return nextEvent;
|
||||
}
|
||||
|
||||
const nextParagraph = text.indexOf("\n\n", afterAction);
|
||||
return nextParagraph === -1 ? text.length : nextParagraph;
|
||||
}
|
||||
|
||||
function stripLegacyInternalRuntimeContext(text: string): string {
|
||||
let next = text;
|
||||
let searchFrom = 0;
|
||||
for (;;) {
|
||||
const headerStart = next.indexOf(LEGACY_INTERNAL_CONTEXT_HEADER, searchFrom);
|
||||
if (headerStart === -1) {
|
||||
return next;
|
||||
}
|
||||
|
||||
const eventStart = headerStart + LEGACY_INTERNAL_CONTEXT_HEADER.length;
|
||||
if (!next.startsWith(LEGACY_INTERNAL_EVENT_MARKER, eventStart)) {
|
||||
searchFrom = eventStart;
|
||||
continue;
|
||||
}
|
||||
|
||||
let blockEnd = findLegacyInternalEventEnd(next, eventStart);
|
||||
if (blockEnd == null) {
|
||||
const nextParagraph = next.indexOf("\n\n", eventStart + LEGACY_INTERNAL_EVENT_MARKER.length);
|
||||
blockEnd = nextParagraph === -1 ? next.length : nextParagraph;
|
||||
} else {
|
||||
while (
|
||||
next.startsWith(
|
||||
`${LEGACY_INTERNAL_EVENT_SEPARATOR}${LEGACY_INTERNAL_EVENT_MARKER}`,
|
||||
blockEnd,
|
||||
)
|
||||
) {
|
||||
const nextEventStart = blockEnd + LEGACY_INTERNAL_EVENT_SEPARATOR.length;
|
||||
const nextEventEnd = findLegacyInternalEventEnd(next, nextEventStart);
|
||||
if (nextEventEnd == null) {
|
||||
break;
|
||||
}
|
||||
blockEnd = nextEventEnd;
|
||||
}
|
||||
}
|
||||
|
||||
const before = next.slice(0, headerStart).trimEnd();
|
||||
const after = next.slice(blockEnd).trimStart();
|
||||
next = before && after ? `${before}\n\n${after}` : `${before}${after}`;
|
||||
searchFrom = Math.max(0, before.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function isRuntimeContextPromptHeader(line: string): boolean {
|
||||
return (
|
||||
line === OPENCLAW_NEXT_TURN_RUNTIME_CONTEXT_HEADER || line === OPENCLAW_RUNTIME_EVENT_HEADER
|
||||
);
|
||||
}
|
||||
|
||||
function stripRuntimeContextPromptPreface(text: string): string {
|
||||
const lines = text.split(/\r?\n/);
|
||||
let changed = false;
|
||||
const output: string[] = [];
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index] ?? "";
|
||||
const nextLine = lines[index + 1] ?? "";
|
||||
if (
|
||||
isRuntimeContextPromptHeader(line.trim()) &&
|
||||
nextLine.trim() === OPENCLAW_RUNTIME_CONTEXT_NOTICE
|
||||
) {
|
||||
changed = true;
|
||||
index += 1;
|
||||
while (index + 1 < lines.length && (lines[index + 1] ?? "").trim() === "") {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
output.push(line);
|
||||
}
|
||||
|
||||
return changed
|
||||
? output
|
||||
.join("\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim()
|
||||
: text;
|
||||
}
|
||||
|
||||
function stripInternalRuntimeContext(text: string): string {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
const withoutDelimitedBlocks = stripDelimitedBlock(
|
||||
text,
|
||||
INTERNAL_RUNTIME_CONTEXT_BEGIN,
|
||||
INTERNAL_RUNTIME_CONTEXT_END,
|
||||
);
|
||||
return stripRuntimeContextPromptPreface(
|
||||
stripLegacyInternalRuntimeContext(withoutDelimitedBlocks),
|
||||
);
|
||||
}
|
||||
|
||||
function buildRemoteBaseUrlPolicy(baseUrl: string): SsrFPolicy | undefined {
|
||||
const trimmed = baseUrl.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:"
|
||||
? { allowedHostnames: [parsed.hostname] }
|
||||
: undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function estimateStringChars(text: string): number {
|
||||
const nonLatinCount =
|
||||
text.match(/[\u2E80-\u9FFF\uA000-\uA4FF\uAC00-\uD7AF\uF900-\uFAFF\u{20000}-\u{2FA1F}]/gu)
|
||||
?.length ?? 0;
|
||||
return text.length + nonLatinCount * (CHARS_PER_TOKEN_ESTIMATE - 1);
|
||||
}
|
||||
|
||||
async function runTasksWithConcurrency<T>(params: {
|
||||
tasks: Array<() => Promise<T>>;
|
||||
limit: number;
|
||||
errorMode?: "continue" | "stop";
|
||||
onTaskError?: (error: unknown, index: number) => void;
|
||||
}): Promise<{ results: T[]; firstError: unknown; hasError: boolean }> {
|
||||
const results: T[] = Array.from({ length: params.tasks.length });
|
||||
let cursor = 0;
|
||||
let firstError: unknown;
|
||||
let hasError = false;
|
||||
const limit = Math.max(1, Math.min(params.limit, params.tasks.length || 1));
|
||||
const workers = Array.from({ length: limit }, async () => {
|
||||
while (cursor < params.tasks.length && !(params.errorMode === "stop" && hasError)) {
|
||||
const index = cursor++;
|
||||
try {
|
||||
results[index] = await params.tasks[index]();
|
||||
} catch (error) {
|
||||
firstError ??= error;
|
||||
hasError = true;
|
||||
params.onTaskError?.(error, index);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.allSettled(workers);
|
||||
return { results, firstError, hasError };
|
||||
}
|
||||
|
||||
function mimeTypeFromFilePath(filePath?: string): string | undefined {
|
||||
const ext = filePath ? path.extname(filePath).toLowerCase() : "";
|
||||
const byExt: Record<string, string> = {
|
||||
".gif": "image/gif",
|
||||
".heic": "image/heic",
|
||||
".heif": "image/heif",
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".webp": "image/webp",
|
||||
};
|
||||
return byExt[ext];
|
||||
}
|
||||
|
||||
const SECRET_PATTERNS: RegExp[] = [
|
||||
/\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD)\b\s*[=:]\s*(["']?)([^\s"'\\]+)\1/g,
|
||||
/[?&](?:access[-_]?token|auth[-_]?token|hook[-_]?token|refresh[-_]?token|api[-_]?key|client[-_]?secret|token|key|secret|password|pass|passwd|auth|signature)=([^&\s"'<>]+)/gi,
|
||||
/"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*"([^"]+)"/g,
|
||||
/--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd)\s+(["']?)([^\s"']+)\1/g,
|
||||
/Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/g,
|
||||
/\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/g,
|
||||
/(^|[\s,;])(?:access_token|refresh_token|api[-_]?key|token|secret|password|passwd)=([^\s&#]+)/g,
|
||||
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
|
||||
/\b(sk-[A-Za-z0-9_-]{8,})\b/g,
|
||||
/\b(ghp_[A-Za-z0-9]{20,})\b/g,
|
||||
/\b(github_pat_[A-Za-z0-9_]{20,})\b/g,
|
||||
/\b(xox[baprs]-[A-Za-z0-9-]{10,})\b/g,
|
||||
/\b(xapp-[A-Za-z0-9-]{10,})\b/g,
|
||||
/\b(gsk_[A-Za-z0-9_-]{10,})\b/g,
|
||||
/\b(AIza[0-9A-Za-z\-_]{20,})\b/g,
|
||||
/\b(pplx-[A-Za-z0-9_-]{10,})\b/g,
|
||||
/\b(npm_[A-Za-z0-9]{10,})\b/g,
|
||||
/\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g,
|
||||
/\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g,
|
||||
];
|
||||
|
||||
function maskToken(token: string): string {
|
||||
if (token.length < 18) {
|
||||
return "***";
|
||||
}
|
||||
return `${token.slice(0, 6)}...${token.slice(-4)}`;
|
||||
}
|
||||
|
||||
function redactPemBlock(block: string): string {
|
||||
const lines = block.split(/\r?\n/).filter(Boolean);
|
||||
if (lines.length < 2) {
|
||||
return "***";
|
||||
}
|
||||
return `${lines[0]}\n...redacted...\n${lines[lines.length - 1]}`;
|
||||
}
|
||||
|
||||
function redactMatch(match: string, groups: string[]): string {
|
||||
if (match.includes("PRIVATE KEY-----")) {
|
||||
return redactPemBlock(match);
|
||||
}
|
||||
const token = groups.findLast((value) => typeof value === "string" && value.length > 0) ?? match;
|
||||
const masked = maskToken(token);
|
||||
return token === match ? masked : match.replace(token, masked);
|
||||
}
|
||||
|
||||
function redactSensitiveText(text: string): string {
|
||||
let next = text;
|
||||
for (const pattern of SECRET_PATTERNS) {
|
||||
next = next.replace(pattern, (...args: string[]) =>
|
||||
redactMatch(args[0] ?? "", args.slice(1, -2)),
|
||||
);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -213,4 +213,34 @@ describe("buildSessionEntry", () => {
|
||||
expect(entry!.content).toBe("Assistant: User-facing summary.\nUser: Actual user follow-up.");
|
||||
expect(entry!.lineMap).toEqual([2, 3]);
|
||||
});
|
||||
|
||||
it("strips internal runtime context blocks", async () => {
|
||||
const jsonlLines = [
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: {
|
||||
role: "user",
|
||||
content: [
|
||||
"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
|
||||
"OpenClaw runtime context (internal):",
|
||||
"This context is runtime-generated, not user-authored. Keep internal details private.",
|
||||
"",
|
||||
"[Internal task completion event]",
|
||||
"source: subagent",
|
||||
"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
|
||||
].join("\n"),
|
||||
},
|
||||
}),
|
||||
JSON.stringify({ type: "message", message: { role: "assistant", content: "NO_REPLY" } }),
|
||||
JSON.stringify({ type: "message", message: { role: "user", content: "Actual user text" } }),
|
||||
];
|
||||
const filePath = path.join(tmpDir, "internal-context-session.jsonl");
|
||||
fsSync.writeFileSync(filePath, jsonlLines.join("\n"));
|
||||
|
||||
const entry = await buildSessionEntry(filePath);
|
||||
|
||||
expect(entry).not.toBeNull();
|
||||
expect(entry!.content).toBe("User: Actual user text");
|
||||
expect(entry!.lineMap).toEqual([3]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,22 +2,7 @@ import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { hashText } from "./hash.js";
|
||||
import { createSubsystemLogger, redactSensitiveText } from "./openclaw-runtime-io.js";
|
||||
import {
|
||||
HEARTBEAT_PROMPT,
|
||||
HEARTBEAT_TOKEN,
|
||||
hasInterSessionUserProvenance,
|
||||
isCompactionCheckpointTranscriptFileName,
|
||||
isCronRunSessionKey,
|
||||
isExecCompletionEvent,
|
||||
isHeartbeatUserMessage,
|
||||
isSessionArchiveArtifactName,
|
||||
isSilentReplyPayloadText,
|
||||
isUsageCountedSessionTranscriptFileName,
|
||||
resolveSessionTranscriptsDirForAgent,
|
||||
stripInboundMetadata,
|
||||
stripInternalRuntimeContext,
|
||||
} from "./openclaw-runtime-session.js";
|
||||
import { HEARTBEAT_PROMPT, HEARTBEAT_TOKEN, getMemoryHostServices } from "./services.js";
|
||||
|
||||
const DREAMING_NARRATIVE_RUN_PREFIX = "dreaming-narrative-";
|
||||
// Keep the historical one-line-per-message export shape for normal turns, but
|
||||
@@ -64,7 +49,8 @@ type SessionTranscriptStoreEntry = {
|
||||
function shouldSkipTranscriptFileForDreaming(absPath: string): boolean {
|
||||
const fileName = path.basename(absPath);
|
||||
return (
|
||||
isSessionArchiveArtifactName(fileName) || isCompactionCheckpointTranscriptFileName(fileName)
|
||||
getMemoryHostServices().session.isSessionArchiveArtifactName(fileName) ||
|
||||
getMemoryHostServices().session.isCompactionCheckpointTranscriptFileName(fileName)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,7 +170,7 @@ export function loadSessionTranscriptClassificationForSessionsDir(
|
||||
if (isDreamingNarrativeSessionStoreKey(sessionKey)) {
|
||||
dreamingTranscriptPaths.add(transcriptPath);
|
||||
}
|
||||
if (isCronRunSessionKey(sessionKey)) {
|
||||
if (getMemoryHostServices().session.isCronRunSessionKey(sessionKey)) {
|
||||
cronRunTranscriptPaths.add(transcriptPath);
|
||||
}
|
||||
}
|
||||
@@ -218,7 +204,7 @@ export function loadSessionTranscriptClassificationForAgent(
|
||||
agentId: string,
|
||||
): SessionTranscriptClassification {
|
||||
return loadSessionTranscriptClassificationForSessionsDir(
|
||||
resolveSessionTranscriptsDirForAgent(agentId),
|
||||
getMemoryHostServices().session.resolveSessionTranscriptsDirForAgent(agentId),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,13 +223,15 @@ function classifySessionTranscriptFromSessionStore(absPath: string): {
|
||||
}
|
||||
|
||||
export async function listSessionFilesForAgent(agentId: string): Promise<string[]> {
|
||||
const dir = resolveSessionTranscriptsDirForAgent(agentId);
|
||||
const dir = getMemoryHostServices().session.resolveSessionTranscriptsDirForAgent(agentId);
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => isUsageCountedSessionTranscriptFileName(name))
|
||||
.filter((name) =>
|
||||
getMemoryHostServices().session.isUsageCountedSessionTranscriptFileName(name),
|
||||
)
|
||||
.map((name) => path.join(dir, name));
|
||||
} catch {
|
||||
return [];
|
||||
@@ -255,7 +243,9 @@ export function sessionPathForFile(absPath: string): string {
|
||||
}
|
||||
|
||||
async function logSessionFileReadFailure(absPath: string, err: unknown): Promise<void> {
|
||||
createSubsystemLogger("memory").debug(`Failed reading session file ${absPath}: ${String(err)}`);
|
||||
getMemoryHostServices()
|
||||
.io.createSubsystemLogger("memory")
|
||||
.debug(`Failed reading session file ${absPath}: ${String(err)}`);
|
||||
}
|
||||
|
||||
function normalizeSessionText(value: string): string {
|
||||
@@ -361,7 +351,7 @@ function stripInboundMetadataForUserRole(text: string, role: "user" | "assistant
|
||||
if (role !== "user") {
|
||||
return text;
|
||||
}
|
||||
return stripInboundMetadata(text);
|
||||
return getMemoryHostServices().session.stripInboundMetadata(text);
|
||||
}
|
||||
|
||||
const GENERATED_SYSTEM_MESSAGE_RE = /^System(?: \(untrusted\))?: \[[^\]]+\]\s*/;
|
||||
@@ -381,12 +371,19 @@ function isGeneratedCronPromptMessage(text: string, role: "user" | "assistant"):
|
||||
}
|
||||
|
||||
function isGeneratedHeartbeatPromptMessage(text: string, role: "user" | "assistant"): boolean {
|
||||
return role === "user" && isHeartbeatUserMessage({ role, content: text }, HEARTBEAT_PROMPT);
|
||||
return (
|
||||
role === "user" &&
|
||||
getMemoryHostServices().session.isHeartbeatUserMessage(
|
||||
{ role, content: text },
|
||||
HEARTBEAT_PROMPT,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeSessionText(text: string, role: "user" | "assistant"): string | null {
|
||||
const strippedInbound = stripInboundMetadataForUserRole(text, role);
|
||||
const strippedInternal = stripInternalRuntimeContext(strippedInbound);
|
||||
const strippedInternal =
|
||||
getMemoryHostServices().session.stripInternalRuntimeContext(strippedInbound);
|
||||
const normalized = normalizeSessionText(strippedInternal);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
@@ -400,7 +397,7 @@ function sanitizeSessionText(text: string, role: "user" | "assistant"): string |
|
||||
if (isGeneratedHeartbeatPromptMessage(normalized, role)) {
|
||||
return null;
|
||||
}
|
||||
if (isSilentReplyPayloadText(normalized)) {
|
||||
if (getMemoryHostServices().session.isSilentReplyPayloadText(normalized)) {
|
||||
return null;
|
||||
}
|
||||
// Assistant-side machinery acks: HEARTBEAT_OK is the canonical "all clear,
|
||||
@@ -411,7 +408,7 @@ function sanitizeSessionText(text: string, role: "user" | "assistant"): string |
|
||||
return null;
|
||||
}
|
||||
const withoutSystemEnvelope = normalized.replace(GENERATED_SYSTEM_MESSAGE_RE, "").trim();
|
||||
if (isExecCompletionEvent(withoutSystemEnvelope)) {
|
||||
if (getMemoryHostServices().session.isExecCompletionEvent(withoutSystemEnvelope)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
@@ -513,7 +510,10 @@ export async function buildSessionEntry(
|
||||
if (message.role !== "user" && message.role !== "assistant") {
|
||||
continue;
|
||||
}
|
||||
if (message.role === "user" && hasInterSessionUserProvenance(message)) {
|
||||
if (
|
||||
message.role === "user" &&
|
||||
getMemoryHostServices().session.hasInterSessionUserProvenance(message)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const rawText = collectRawSessionText(message.content);
|
||||
@@ -534,7 +534,7 @@ export async function buildSessionEntry(
|
||||
if (generatedByDreamingNarrative || generatedByCronRun) {
|
||||
continue;
|
||||
}
|
||||
const safe = redactSensitiveText(text, { mode: "tools" });
|
||||
const safe = getMemoryHostServices().io.redactSensitiveText(text, { mode: "tools" });
|
||||
const label = message.role === "user" ? "User" : "Assistant";
|
||||
const renderedLines = renderSessionExportLines(label, safe);
|
||||
const timestampMs = parseSessionTimestampMs(
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
// Focused runtime contract for memory CLI/UI helpers.
|
||||
// CLI helpers are core-bound by the workspace facade. The package surface
|
||||
// intentionally keeps no core import.
|
||||
|
||||
export { formatErrorMessage, withManager } from "./host/openclaw-runtime-cli.js";
|
||||
export { formatHelpExamples } from "./host/openclaw-runtime-cli.js";
|
||||
export { resolveCommandSecretRefsViaGateway } from "./host/openclaw-runtime-cli.js";
|
||||
export { withProgress, withProgressTotals } from "./host/openclaw-runtime-cli.js";
|
||||
export { defaultRuntime } from "./host/openclaw-runtime-cli.js";
|
||||
export { formatDocsLink } from "./host/openclaw-runtime-cli.js";
|
||||
export { colorize, isRich, theme } from "./host/openclaw-runtime-cli.js";
|
||||
export { isVerbose, setVerbose } from "./host/openclaw-runtime-cli.js";
|
||||
export { shortenHomeInString, shortenHomePath } from "./host/openclaw-runtime-cli.js";
|
||||
export type MemoryHostCliRuntime = never;
|
||||
|
||||
@@ -1,41 +1,16 @@
|
||||
// Focused runtime contract for memory plugin config/state/helpers.
|
||||
// Package-local memory runtime contract. Core binds richer OpenClaw services
|
||||
// through src/memory-host-sdk; this package stays host-agnostic.
|
||||
|
||||
export type { AnyAgentTool } from "./host/openclaw-runtime-agent.js";
|
||||
export { resolveCronStyleNow } from "./host/openclaw-runtime-agent.js";
|
||||
export { DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR } from "./host/openclaw-runtime-agent.js";
|
||||
export { resolveDefaultAgentId, resolveSessionAgentId } from "./host/openclaw-runtime-agent.js";
|
||||
export { resolveMemorySearchConfig } from "./host/openclaw-runtime-agent.js";
|
||||
export {
|
||||
asToolParamsRecord,
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readStringParam,
|
||||
} from "./host/openclaw-runtime-agent.js";
|
||||
export { SILENT_REPLY_TOKEN } from "./host/openclaw-runtime-session.js";
|
||||
export { parseNonNegativeByteSize } from "./host/openclaw-runtime-config.js";
|
||||
SILENT_REPLY_TOKEN,
|
||||
getMemoryHostServices,
|
||||
setMemoryHostServices,
|
||||
withMemoryHostServices,
|
||||
type MemoryHostServices,
|
||||
} from "./host/services.js";
|
||||
export {
|
||||
getRuntimeConfig,
|
||||
/** @deprecated Use getRuntimeConfig(), or pass the already loaded config through the call path. */
|
||||
loadConfig,
|
||||
} from "./host/openclaw-runtime-config.js";
|
||||
export { resolveStateDir } from "./host/openclaw-runtime-config.js";
|
||||
export { resolveSessionTranscriptsDirForAgent } from "./host/openclaw-runtime-config.js";
|
||||
export { emptyPluginConfigSchema } from "./host/openclaw-runtime-memory.js";
|
||||
export {
|
||||
buildActiveMemoryPromptSection,
|
||||
getMemoryCapabilityRegistration,
|
||||
listActiveMemoryPublicArtifacts,
|
||||
} from "./host/openclaw-runtime-memory.js";
|
||||
export { parseAgentSessionKey } from "./host/openclaw-runtime-agent.js";
|
||||
export type { OpenClawConfig } from "./host/openclaw-runtime-config.js";
|
||||
export type { MemoryCitationsMode } from "./host/openclaw-runtime-config.js";
|
||||
export type {
|
||||
MemoryFlushPlan,
|
||||
MemoryFlushPlanResolver,
|
||||
MemoryPluginCapability,
|
||||
MemoryPluginPublicArtifact,
|
||||
MemoryPluginPublicArtifactsProvider,
|
||||
MemoryPluginRuntime,
|
||||
MemoryPromptSectionBuilder,
|
||||
} from "./host/openclaw-runtime-memory.js";
|
||||
export type { OpenClawPluginApi } from "./host/openclaw-runtime-memory.js";
|
||||
resolveMemorySearchConfig,
|
||||
resolveStateDir,
|
||||
type MemoryCitationsMode,
|
||||
type OpenClawConfig,
|
||||
} from "./host/config-utils.js";
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
# Thread memory isolation
|
||||
|
||||
```yaml qa-scenario
|
||||
id: thread-memory-isolation
|
||||
title: Thread memory isolation
|
||||
surface: memory
|
||||
coverage:
|
||||
primary:
|
||||
- memory.thread-isolation
|
||||
secondary:
|
||||
- channels.threads
|
||||
objective: Verify a memory-backed answer requested inside a thread stays in-thread and does not leak into the root channel.
|
||||
successCriteria:
|
||||
- Agent uses memory tools inside the thread.
|
||||
- The hidden fact is answered correctly in the thread.
|
||||
- No root-channel outbound message leaks during the threaded memory reply.
|
||||
docsRefs:
|
||||
- docs/concepts/memory-search.md
|
||||
- docs/channels/qa-channel.md
|
||||
- docs/channels/group-messages.md
|
||||
codeRefs:
|
||||
- extensions/memory-core/src/tools.ts
|
||||
- extensions/qa-channel/src/protocol.ts
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify a memory-backed answer requested inside a thread stays in-thread and does not leak into the root channel.
|
||||
config:
|
||||
memoryFact: "Thread-hidden codename: ORBIT-22."
|
||||
memoryQuery: "hidden thread codename ORBIT-22"
|
||||
expectedNeedle: "ORBIT-22"
|
||||
channelId: qa-room
|
||||
channelTitle: QA Room
|
||||
threadTitle: "Thread memory QA"
|
||||
prompt: "@openclaw Thread memory check: what is the hidden thread codename stored only in memory? Use memory tools first and reply only in this thread."
|
||||
promptSnippet: "Thread memory check"
|
||||
```
|
||||
|
||||
```yaml qa-flow
|
||||
steps:
|
||||
- name: answers the memory-backed fact inside the thread only
|
||||
actions:
|
||||
- call: reset
|
||||
- call: fs.writeFile
|
||||
args:
|
||||
- expr: "path.join(env.gateway.workspaceDir, 'MEMORY.md')"
|
||||
- expr: "`${config.memoryFact}\\n`"
|
||||
- utf8
|
||||
- call: forceMemoryIndex
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
query:
|
||||
expr: config.memoryQuery
|
||||
expectedNeedle:
|
||||
expr: config.expectedNeedle
|
||||
- call: handleQaAction
|
||||
saveAs: threadPayload
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
action: thread-create
|
||||
args:
|
||||
channelId:
|
||||
expr: config.channelId
|
||||
title:
|
||||
expr: config.threadTitle
|
||||
- set: threadId
|
||||
value:
|
||||
expr: "threadPayload?.thread?.id"
|
||||
- assert:
|
||||
expr: Boolean(threadId)
|
||||
message: missing thread id for memory isolation check
|
||||
- set: beforeCursor
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: state.addInboundMessage
|
||||
args:
|
||||
- conversation:
|
||||
id:
|
||||
expr: config.channelId
|
||||
kind: channel
|
||||
title:
|
||||
expr: config.channelTitle
|
||||
senderId: alice
|
||||
senderName: Alice
|
||||
text:
|
||||
expr: config.prompt
|
||||
threadId:
|
||||
ref: threadId
|
||||
threadTitle:
|
||||
expr: config.threadTitle
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === config.channelId && candidate.threadId === threadId && candidate.text.includes(config.expectedNeedle)"
|
||||
- expr: liveTurnTimeoutMs(env, 45000)
|
||||
- assert:
|
||||
expr: "!state.getSnapshot().messages.slice(beforeCursor).some((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === config.channelId && !candidate.threadId)"
|
||||
message: threaded memory answer leaked into root channel
|
||||
- assert:
|
||||
expr: "!env.mock || (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).filter((request) => String(request.allInputText ?? '').includes(config.promptSnippet)).some((request) => request.plannedToolName === 'memory_search')"
|
||||
message: expected memory_search in thread memory flow
|
||||
detailsExpr: outbound.text
|
||||
```
|
||||
111
qa/scenarios/memory/thread-memory-isolation.yaml
Normal file
111
qa/scenarios/memory/thread-memory-isolation.yaml
Normal file
@@ -0,0 +1,111 @@
|
||||
title: Thread memory isolation
|
||||
scenario:
|
||||
id: thread-memory-isolation
|
||||
surface: memory
|
||||
coverage:
|
||||
primary:
|
||||
- memory.thread-isolation
|
||||
secondary:
|
||||
- channels.threads
|
||||
objective: Verify a memory-backed answer requested inside a thread stays in-thread and does not leak into the root channel.
|
||||
successCriteria:
|
||||
- Agent uses memory tools inside the thread.
|
||||
- The hidden fact is answered correctly in the thread.
|
||||
- No root-channel outbound message leaks during the threaded memory reply.
|
||||
docsRefs:
|
||||
- docs/concepts/memory-search.md
|
||||
- docs/channels/qa-channel.md
|
||||
- docs/channels/group-messages.md
|
||||
codeRefs:
|
||||
- extensions/memory-core/src/tools.ts
|
||||
- extensions/qa-channel/src/protocol.ts
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify a memory-backed answer requested inside a thread stays in-thread and does not leak into the root channel.
|
||||
config:
|
||||
memoryFact: "Thread-hidden codename: ORBIT-22."
|
||||
memoryQuery: "hidden thread codename ORBIT-22"
|
||||
expectedNeedle: "ORBIT-22"
|
||||
channelId: qa-room
|
||||
channelTitle: QA Room
|
||||
threadTitle: "Thread memory QA"
|
||||
prompt: "@openclaw Thread memory check: what is the hidden thread codename stored only in memory? Use memory tools first and reply only in this thread."
|
||||
promptSnippet: "Thread memory check"
|
||||
flow:
|
||||
steps:
|
||||
- name: answers the memory-backed fact inside the thread only
|
||||
actions:
|
||||
- call: reset
|
||||
- call: fs.writeFile
|
||||
args:
|
||||
- expr: "path.join(env.gateway.workspaceDir, 'MEMORY.md')"
|
||||
- expr: "`${config.memoryFact}\\n`"
|
||||
- utf8
|
||||
- call: forceMemoryIndex
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
query:
|
||||
expr: config.memoryQuery
|
||||
expectedNeedle:
|
||||
expr: config.expectedNeedle
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: handleQaAction
|
||||
saveAs: threadPayload
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
action: thread-create
|
||||
args:
|
||||
channelId:
|
||||
expr: config.channelId
|
||||
title:
|
||||
expr: config.threadTitle
|
||||
- set: threadId
|
||||
value:
|
||||
expr: "threadPayload?.thread?.id"
|
||||
- assert:
|
||||
expr: Boolean(threadId)
|
||||
message: missing thread id for memory isolation check
|
||||
- set: beforeCursor
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: state.addInboundMessage
|
||||
args:
|
||||
- conversation:
|
||||
id:
|
||||
expr: config.channelId
|
||||
kind: channel
|
||||
title:
|
||||
expr: config.channelTitle
|
||||
senderId: alice
|
||||
senderName: Alice
|
||||
text:
|
||||
expr: config.prompt
|
||||
threadId:
|
||||
ref: threadId
|
||||
threadTitle:
|
||||
expr: config.threadTitle
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === config.channelId && candidate.threadId === threadId && candidate.text.includes(config.expectedNeedle)"
|
||||
- expr: liveTurnTimeoutMs(env, 45000)
|
||||
- assert:
|
||||
expr: "!state.getSnapshot().messages.slice(beforeCursor).some((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === config.channelId && !candidate.threadId)"
|
||||
message: threaded memory answer leaked into root channel
|
||||
- assert:
|
||||
expr: "!env.mock || (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).filter((request) => String(request.allInputText ?? '').includes(config.promptSnippet)).some((request) => request.plannedToolName === 'memory_search')"
|
||||
message: expected memory_search in thread memory flow
|
||||
detailsExpr: outbound.text
|
||||
@@ -101,6 +101,9 @@ function getColorForConsole(): ChalkInstance {
|
||||
if (process.env.NO_COLOR && !hasForceColor) {
|
||||
return new Chalk({ level: 0 });
|
||||
}
|
||||
if (hasForceColor) {
|
||||
return new Chalk({ level: 1 });
|
||||
}
|
||||
const hasTty = process.stdout.isTTY || process.stderr.isTTY;
|
||||
return hasTty || isRichConsoleEnv() ? new Chalk({ level: 1 }) : new Chalk({ level: 0 });
|
||||
}
|
||||
|
||||
@@ -1 +1,71 @@
|
||||
export * from "../../packages/memory-host-sdk/src/engine-embeddings.js";
|
||||
export {
|
||||
getMemoryEmbeddingProvider,
|
||||
listMemoryEmbeddingProviders,
|
||||
listRegisteredMemoryEmbeddingProviderAdapters,
|
||||
listRegisteredMemoryEmbeddingProviders,
|
||||
} from "../plugins/memory-embedding-provider-runtime.js";
|
||||
export type {
|
||||
MemoryEmbeddingBatchChunk,
|
||||
MemoryEmbeddingBatchOptions,
|
||||
MemoryEmbeddingProvider,
|
||||
MemoryEmbeddingProviderAdapter,
|
||||
MemoryEmbeddingProviderCreateOptions,
|
||||
MemoryEmbeddingProviderCreateResult,
|
||||
MemoryEmbeddingProviderRuntime,
|
||||
} from "../plugins/memory-embedding-providers.js";
|
||||
export { createLocalEmbeddingProvider, DEFAULT_LOCAL_MODEL } from "./host/embeddings.js";
|
||||
export { extractBatchErrorMessage, formatUnavailableBatchError } from "./host/batch-error-utils.js";
|
||||
export { postJsonWithRetry } from "./host/batch-http.js";
|
||||
export { applyEmbeddingBatchOutputLine } from "./host/batch-output.js";
|
||||
export {
|
||||
EMBEDDING_BATCH_ENDPOINT,
|
||||
type EmbeddingBatchStatus,
|
||||
type ProviderBatchOutputLine,
|
||||
} from "./host/batch-provider-common.js";
|
||||
export {
|
||||
buildEmbeddingBatchGroupOptions,
|
||||
runEmbeddingBatchGroups,
|
||||
type EmbeddingBatchExecutionParams,
|
||||
} from "./host/batch-runner.js";
|
||||
export {
|
||||
resolveBatchCompletionFromStatus,
|
||||
resolveCompletedBatchResult,
|
||||
throwIfBatchTerminalFailure,
|
||||
type BatchCompletionResult,
|
||||
} from "./host/batch-status.js";
|
||||
export { uploadBatchJsonlFile } from "./host/batch-upload.js";
|
||||
export {
|
||||
buildBatchHeaders,
|
||||
normalizeBatchBaseUrl,
|
||||
type BatchHttpClientConfig,
|
||||
} from "./host/batch-utils.js";
|
||||
export { enforceEmbeddingMaxInputTokens } from "./host/embedding-chunk-limits.js";
|
||||
export {
|
||||
isMissingEmbeddingApiKeyError,
|
||||
mapBatchEmbeddingsByIndex,
|
||||
sanitizeEmbeddingCacheHeaders,
|
||||
} from "./host/embedding-provider-adapter-utils.js";
|
||||
export { sanitizeAndNormalizeEmbedding } from "./host/embedding-vectors.js";
|
||||
export { debugEmbeddingsLog } from "./host/embeddings-debug.js";
|
||||
export { normalizeEmbeddingModelWithPrefixes } from "./host/embeddings-model-normalize.js";
|
||||
export {
|
||||
resolveRemoteEmbeddingBearerClient,
|
||||
type RemoteEmbeddingProviderId,
|
||||
} from "./host/embeddings-remote-client.js";
|
||||
export {
|
||||
createRemoteEmbeddingProvider,
|
||||
resolveRemoteEmbeddingClient,
|
||||
type RemoteEmbeddingClient,
|
||||
} from "./host/embeddings-remote-provider.js";
|
||||
export { fetchRemoteEmbeddingVectors } from "./host/embeddings-remote-fetch.js";
|
||||
export {
|
||||
estimateStructuredEmbeddingInputBytes,
|
||||
estimateUtf8Bytes,
|
||||
} from "./host/embedding-input-limits.js";
|
||||
export { hasNonTextEmbeddingParts, type EmbeddingInput } from "./host/embedding-inputs.js";
|
||||
export { buildRemoteBaseUrlPolicy, withRemoteHttpResponse } from "./host/remote-http.js";
|
||||
export {
|
||||
buildCaseInsensitiveExtensionGlob,
|
||||
classifyMemoryMultimodalPath,
|
||||
getMemoryMultimodalExtensions,
|
||||
} from "./host/multimodal.js";
|
||||
|
||||
@@ -1 +1,46 @@
|
||||
export * from "../../packages/memory-host-sdk/src/engine-foundation.js";
|
||||
export {
|
||||
resolveAgentContextLimits,
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
export {
|
||||
resolveMemorySearchConfig,
|
||||
resolveMemorySearchSyncConfig,
|
||||
type ResolvedMemorySearchConfig,
|
||||
type ResolvedMemorySearchSyncConfig,
|
||||
} from "../agents/memory-search.js";
|
||||
export { parseDurationMs } from "../cli/parse-duration.js";
|
||||
export { loadConfig } from "../config/config.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export { resolveStateDir } from "../config/paths.js";
|
||||
export { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
|
||||
export {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
type SecretInput,
|
||||
} from "../config/types.secrets.js";
|
||||
export type { SessionSendPolicyConfig } from "../config/types.base.js";
|
||||
export type {
|
||||
MemoryBackend,
|
||||
MemoryCitationsMode,
|
||||
MemoryQmdConfig,
|
||||
MemoryQmdIndexPath,
|
||||
MemoryQmdMcporterConfig,
|
||||
MemoryQmdSearchMode,
|
||||
} from "../config/types.memory.js";
|
||||
export type { MemorySearchConfig } from "../config/types.tools.js";
|
||||
export { writeFileWithinRoot } from "../infra/fs-safe.js";
|
||||
export { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
export { detectMime } from "../media/mime.js";
|
||||
export { onSessionTranscriptUpdate } from "../sessions/transcript-events.js";
|
||||
export { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
export { runTasksWithConcurrency } from "../utils/run-with-concurrency.js";
|
||||
export { splitShellArgs } from "../utils/shell-argv.js";
|
||||
export {
|
||||
resolveUserPath,
|
||||
shortenHomeInString,
|
||||
shortenHomePath,
|
||||
truncateUtf16Safe,
|
||||
} from "../utils.js";
|
||||
|
||||
@@ -1 +1,24 @@
|
||||
export * from "../../packages/memory-host-sdk/src/engine-qmd.js";
|
||||
export { parseUsageCountedSessionIdFromFileName } from "../config/sessions/artifacts.js";
|
||||
export { extractKeywords, isQueryStopWordToken } from "./host/query-expansion.js";
|
||||
export {
|
||||
buildSessionEntry,
|
||||
listSessionFilesForAgent,
|
||||
loadDreamingNarrativeTranscriptPathSetForAgent,
|
||||
loadSessionTranscriptClassificationForAgent,
|
||||
normalizeSessionTranscriptPathForComparison,
|
||||
sessionPathForFile,
|
||||
type BuildSessionEntryOptions,
|
||||
type SessionFileEntry,
|
||||
type SessionTranscriptClassification,
|
||||
} from "./host/session-files.js";
|
||||
export { parseQmdQueryJson, type QmdQueryResult } from "./host/qmd-query-parser.js";
|
||||
export {
|
||||
deriveQmdScopeChannel,
|
||||
deriveQmdScopeChatType,
|
||||
isQmdScopeAllowed,
|
||||
} from "./host/qmd-scope.js";
|
||||
export {
|
||||
checkQmdBinaryAvailability,
|
||||
resolveCliSpawnInvocation,
|
||||
runCliCommand,
|
||||
} from "./host/qmd-process.js";
|
||||
|
||||
@@ -1 +1,46 @@
|
||||
export * from "../../packages/memory-host-sdk/src/engine-storage.js";
|
||||
export {
|
||||
buildFileEntry,
|
||||
buildMultimodalChunkForIndexing,
|
||||
chunkMarkdown,
|
||||
cosineSimilarity,
|
||||
ensureDir,
|
||||
hashText,
|
||||
listMemoryFiles,
|
||||
normalizeExtraMemoryPaths,
|
||||
parseEmbedding,
|
||||
remapChunkLines,
|
||||
runWithConcurrency,
|
||||
type MemoryChunk,
|
||||
type MemoryFileEntry,
|
||||
} from "./host/internal.js";
|
||||
export { readMemoryFile } from "./host/read-file.js";
|
||||
export {
|
||||
buildMemoryReadResult,
|
||||
buildMemoryReadResultFromSlice,
|
||||
DEFAULT_MEMORY_READ_LINES,
|
||||
DEFAULT_MEMORY_READ_MAX_CHARS,
|
||||
type MemoryReadResult,
|
||||
} from "./host/read-file-shared.js";
|
||||
export { resolveMemoryBackendConfig } from "./host/backend-config.js";
|
||||
export type {
|
||||
ResolvedMemoryBackendConfig,
|
||||
ResolvedQmdConfig,
|
||||
ResolvedQmdMcporterConfig,
|
||||
} from "./host/backend-config.js";
|
||||
export type {
|
||||
MemoryEmbeddingProbeResult,
|
||||
MemoryProviderStatus,
|
||||
MemorySearchManager,
|
||||
MemorySearchRuntimeDebug,
|
||||
MemorySearchResult,
|
||||
MemorySource,
|
||||
MemorySyncProgressUpdate,
|
||||
} from "./host/types.js";
|
||||
export { ensureMemoryIndexSchema } from "./host/memory-schema.js";
|
||||
export { loadSqliteVecExtension } from "./host/sqlite-vec.js";
|
||||
export {
|
||||
closeMemorySqliteWalMaintenance,
|
||||
configureMemorySqliteWalMaintenance,
|
||||
requireNodeSqlite,
|
||||
} from "./host/sqlite.js";
|
||||
export { isFileMissingError, statRegularFile } from "./host/fs-utils.js";
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
export * from "../../packages/memory-host-sdk/src/engine.js";
|
||||
export * from "./engine-foundation.js";
|
||||
export * from "./engine-storage.js";
|
||||
export * from "./engine-embeddings.js";
|
||||
export * from "./engine-qmd.js";
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
export * from "../../packages/memory-host-sdk/src/runtime-cli.js";
|
||||
export { formatErrorMessage, withManager } from "../cli/cli-utils.js";
|
||||
export { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
|
||||
export { formatHelpExamples } from "../cli/help-format.js";
|
||||
export { withProgress, withProgressTotals } from "../cli/progress.js";
|
||||
export { isVerbose, setVerbose } from "../globals.js";
|
||||
export { defaultRuntime } from "../runtime.js";
|
||||
export { formatDocsLink } from "../terminal/links.js";
|
||||
export { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
export { shortenHomeInString, shortenHomePath } from "../utils.js";
|
||||
|
||||
@@ -1 +1,39 @@
|
||||
export * from "../../packages/memory-host-sdk/src/runtime-core.js";
|
||||
export { DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR } from "../agents/pi-settings.js";
|
||||
export {
|
||||
asToolParamsRecord,
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readStringParam,
|
||||
type AnyAgentTool,
|
||||
} from "../agents/tools/common.js";
|
||||
export { resolveCronStyleNow } from "../agents/current-time.js";
|
||||
export { resolveDefaultAgentId, resolveSessionAgentId } from "../agents/agent-scope.js";
|
||||
export { resolveMemorySearchConfig } from "../agents/memory-search.js";
|
||||
export { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
export { parseNonNegativeByteSize } from "../config/byte-size.js";
|
||||
export {
|
||||
getRuntimeConfig,
|
||||
/** @deprecated Use getRuntimeConfig(), or pass the already loaded config through the call path. */
|
||||
loadConfig,
|
||||
} from "../config/config.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export { resolveStateDir } from "../config/paths.js";
|
||||
export { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
|
||||
export type { MemoryCitationsMode } from "../config/types.memory.js";
|
||||
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
export {
|
||||
buildMemoryPromptSection as buildActiveMemoryPromptSection,
|
||||
getMemoryCapabilityRegistration,
|
||||
listActiveMemoryPublicArtifacts,
|
||||
} from "../plugins/memory-state.js";
|
||||
export type {
|
||||
MemoryFlushPlan,
|
||||
MemoryFlushPlanResolver,
|
||||
MemoryPluginCapability,
|
||||
MemoryPluginPublicArtifact,
|
||||
MemoryPluginPublicArtifactsProvider,
|
||||
MemoryPluginRuntime,
|
||||
MemoryPromptSectionBuilder,
|
||||
} from "../plugins/memory-state.js";
|
||||
export type { OpenClawPluginApi } from "../plugins/types.js";
|
||||
export { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
|
||||
@@ -1 +1,16 @@
|
||||
export * from "../../packages/memory-host-sdk/src/runtime-files.js";
|
||||
export { readAgentMemoryFile, readMemoryFile } from "./host/read-file.js";
|
||||
export { listMemoryFiles, normalizeExtraMemoryPaths } from "./host/internal.js";
|
||||
export {
|
||||
buildMemoryReadResult,
|
||||
buildMemoryReadResultFromSlice,
|
||||
DEFAULT_MEMORY_READ_LINES,
|
||||
DEFAULT_MEMORY_READ_MAX_CHARS,
|
||||
type MemoryReadResult,
|
||||
} from "./host/read-file-shared.js";
|
||||
export { resolveMemoryBackendConfig } from "./host/backend-config.js";
|
||||
export type {
|
||||
ResolvedMemoryBackendConfig,
|
||||
ResolvedQmdConfig,
|
||||
ResolvedQmdMcporterConfig,
|
||||
} from "./host/backend-config.js";
|
||||
export type { MemorySearchResult, MemorySearchRuntimeDebug } from "./host/types.js";
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from "../../packages/memory-host-sdk/src/runtime.js";
|
||||
export * from "./runtime-core.js";
|
||||
export * from "./runtime-cli.js";
|
||||
export * from "./runtime-files.js";
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
export * from "../../packages/memory-host-sdk/src/engine-embeddings.js";
|
||||
export {
|
||||
getMemoryEmbeddingProvider,
|
||||
listMemoryEmbeddingProviders,
|
||||
listRegisteredMemoryEmbeddingProviderAdapters,
|
||||
listRegisteredMemoryEmbeddingProviders,
|
||||
} from "../plugins/memory-embedding-provider-runtime.js";
|
||||
export {
|
||||
clearMemoryEmbeddingProviders,
|
||||
registerMemoryEmbeddingProvider,
|
||||
} from "../plugins/memory-embedding-providers.js";
|
||||
export type {
|
||||
MemoryEmbeddingBatchChunk,
|
||||
MemoryEmbeddingBatchOptions,
|
||||
MemoryEmbeddingProvider,
|
||||
MemoryEmbeddingProviderAdapter,
|
||||
MemoryEmbeddingProviderCreateOptions,
|
||||
MemoryEmbeddingProviderCreateResult,
|
||||
MemoryEmbeddingProviderRuntime,
|
||||
} from "../plugins/memory-embedding-providers.js";
|
||||
|
||||
@@ -1 +1,56 @@
|
||||
export * from "../../packages/memory-host-sdk/src/engine-foundation.js";
|
||||
export {
|
||||
CHARS_PER_TOKEN_ESTIMATE,
|
||||
HEARTBEAT_PROMPT,
|
||||
HEARTBEAT_TOKEN,
|
||||
SILENT_REPLY_TOKEN,
|
||||
getMemoryHostServices,
|
||||
setMemoryHostServices,
|
||||
withMemoryHostServices,
|
||||
type MemoryHostServices,
|
||||
} from "../../packages/memory-host-sdk/src/engine-foundation.js";
|
||||
export {
|
||||
resolveAgentContextLimits,
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
export {
|
||||
resolveMemorySearchConfig,
|
||||
resolveMemorySearchSyncConfig,
|
||||
type ResolvedMemorySearchConfig,
|
||||
type ResolvedMemorySearchSyncConfig,
|
||||
} from "../agents/memory-search.js";
|
||||
export { parseDurationMs } from "../cli/parse-duration.js";
|
||||
export { loadConfig } from "../config/config.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export { resolveStateDir } from "../config/paths.js";
|
||||
export { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
|
||||
export {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
type SecretInput,
|
||||
} from "../config/types.secrets.js";
|
||||
export type { SessionSendPolicyConfig } from "../config/types.base.js";
|
||||
export type {
|
||||
MemoryBackend,
|
||||
MemoryCitationsMode,
|
||||
MemoryQmdConfig,
|
||||
MemoryQmdIndexPath,
|
||||
MemoryQmdMcporterConfig,
|
||||
MemoryQmdSearchMode,
|
||||
} from "../config/types.memory.js";
|
||||
export type { MemorySearchConfig } from "../config/types.tools.js";
|
||||
export { writeFileWithinRoot } from "../infra/fs-safe.js";
|
||||
export { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
export { detectMime } from "../media/mime.js";
|
||||
export { onSessionTranscriptUpdate } from "../sessions/transcript-events.js";
|
||||
export { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
export { runTasksWithConcurrency } from "../utils/run-with-concurrency.js";
|
||||
export { splitShellArgs } from "../utils/shell-argv.js";
|
||||
export {
|
||||
resolveUserPath,
|
||||
shortenHomeInString,
|
||||
shortenHomePath,
|
||||
truncateUtf16Safe,
|
||||
} from "../utils.js";
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
export * from "../../packages/memory-host-sdk/src/runtime-cli.js";
|
||||
export { formatErrorMessage, withManager } from "../cli/cli-utils.js";
|
||||
export { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
|
||||
export { formatHelpExamples } from "../cli/help-format.js";
|
||||
export { withProgress, withProgressTotals } from "../cli/progress.js";
|
||||
export { isVerbose, setVerbose } from "../globals.js";
|
||||
export { defaultRuntime } from "../runtime.js";
|
||||
export { formatDocsLink } from "../terminal/links.js";
|
||||
export { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
export { shortenHomeInString, shortenHomePath } from "../utils.js";
|
||||
|
||||
@@ -1,13 +1,49 @@
|
||||
export * from "../../packages/memory-host-sdk/src/runtime-core.js";
|
||||
export {
|
||||
SILENT_REPLY_TOKEN,
|
||||
getMemoryHostServices,
|
||||
setMemoryHostServices,
|
||||
withMemoryHostServices,
|
||||
type MemoryHostServices,
|
||||
} from "../../packages/memory-host-sdk/src/runtime-core.js";
|
||||
export { DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR } from "../agents/pi-settings.js";
|
||||
export {
|
||||
asToolParamsRecord,
|
||||
jsonResult,
|
||||
readNumberParam,
|
||||
readStringParam,
|
||||
type AnyAgentTool,
|
||||
} from "../agents/tools/common.js";
|
||||
export { resolveCronStyleNow } from "../agents/current-time.js";
|
||||
export { resolveDefaultAgentId, resolveSessionAgentId } from "../agents/agent-scope.js";
|
||||
export { resolveMemorySearchConfig } from "../agents/memory-search.js";
|
||||
export { parseNonNegativeByteSize } from "../config/byte-size.js";
|
||||
export { getRuntimeConfig, loadConfig } from "../config/config.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export { resolveStateDir } from "../config/paths.js";
|
||||
export { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
|
||||
export type { MemoryCitationsMode } from "../config/types.memory.js";
|
||||
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
export type {
|
||||
MemoryCorpusGetResult,
|
||||
MemoryCorpusSearchResult,
|
||||
MemoryCorpusSupplement,
|
||||
MemoryCorpusSupplementRegistration,
|
||||
MemoryFlushPlan,
|
||||
MemoryFlushPlanResolver,
|
||||
MemoryPluginCapability,
|
||||
MemoryPluginPublicArtifact,
|
||||
MemoryPluginPublicArtifactsProvider,
|
||||
MemoryPluginRuntime,
|
||||
MemoryPromptSectionBuilder,
|
||||
} from "../plugins/memory-state.js";
|
||||
export {
|
||||
buildMemoryPromptSection as buildActiveMemoryPromptSection,
|
||||
clearMemoryPluginState,
|
||||
getMemoryCapabilityRegistration,
|
||||
listActiveMemoryPublicArtifacts,
|
||||
listMemoryCorpusSupplements,
|
||||
registerMemoryCapability,
|
||||
registerMemoryCorpusSupplement,
|
||||
} from "../plugins/memory-state.js";
|
||||
export type { OpenClawPluginApi } from "../plugins/types.js";
|
||||
export { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
|
||||
@@ -55,20 +55,6 @@ const MEMORY_HOST_SDK_EXPORTS = [
|
||||
"./secret",
|
||||
"./status",
|
||||
] as const;
|
||||
const MEMORY_HOST_SDK_ALLOWED_CORE_BRIDGE_FILES = [
|
||||
"packages/memory-host-sdk/src/host/openclaw-runtime.ts",
|
||||
] as const;
|
||||
const MEMORY_HOST_SDK_RUNTIME_ADAPTER_FILES = [
|
||||
"packages/memory-host-sdk/src/host/openclaw-runtime-agent.ts",
|
||||
"packages/memory-host-sdk/src/host/openclaw-runtime-auth.ts",
|
||||
"packages/memory-host-sdk/src/host/openclaw-runtime-cli.ts",
|
||||
"packages/memory-host-sdk/src/host/openclaw-runtime-config.ts",
|
||||
"packages/memory-host-sdk/src/host/openclaw-runtime-io.ts",
|
||||
"packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts",
|
||||
"packages/memory-host-sdk/src/host/openclaw-runtime-network.ts",
|
||||
"packages/memory-host-sdk/src/host/openclaw-runtime-session.ts",
|
||||
] as const;
|
||||
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Test helper lets assertions ascribe JSON file shape.
|
||||
function readJsonFile<T>(relativePath: string): T {
|
||||
return JSON.parse(readFileSync(resolve(REPO_ROOT, relativePath), "utf8")) as T;
|
||||
@@ -253,12 +239,11 @@ describe("opt-in extension package boundaries", () => {
|
||||
expect(source, target).not.toContain("src/memory-host-sdk/");
|
||||
}
|
||||
|
||||
expect(collectCoreReferenceFiles("packages/memory-host-sdk/src")).toEqual([
|
||||
...MEMORY_HOST_SDK_ALLOWED_CORE_BRIDGE_FILES,
|
||||
]);
|
||||
expect(collectOpenClawRuntimeDirectImportFiles("packages/memory-host-sdk/src")).toEqual([
|
||||
...MEMORY_HOST_SDK_RUNTIME_ADAPTER_FILES,
|
||||
]);
|
||||
expect(collectCoreReferenceFiles("packages/memory-host-sdk/src")).toEqual([]);
|
||||
expect(collectOpenClawRuntimeDirectImportFiles("packages/memory-host-sdk/src")).toEqual([]);
|
||||
expect(existsSync(resolve(REPO_ROOT, "packages/memory-host-sdk/src/host/services.ts"))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps plugin-package-contract independent from core internals", () => {
|
||||
|
||||
@@ -30,14 +30,16 @@ describe("plugin-boundary-report", () => {
|
||||
unusedReservedCount?: unknown;
|
||||
};
|
||||
memoryHostSdk?: {
|
||||
packageCoreReferenceFileCount?: unknown;
|
||||
sourceBridgeFileCount?: unknown;
|
||||
implementation?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
expect(summary.pluginSdk?.crossOwnerReservedImportCount).toBe(0);
|
||||
expect(summary.pluginSdk?.unusedReservedCount).toBe(0);
|
||||
expect(["private-core-bridge", "private-package-core-integrated"]).toContain(
|
||||
summary.memoryHostSdk?.implementation,
|
||||
);
|
||||
expect(summary.memoryHostSdk?.sourceBridgeFileCount).toBe(0);
|
||||
expect(summary.memoryHostSdk?.packageCoreReferenceFileCount).toBe(0);
|
||||
expect(summary.memoryHostSdk?.implementation).toBe("package-owned");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user