Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions examples/openclaw-plugin/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export type MemoryOpenVikingConfig = {
emitStandardDiagnostics?: boolean;
/** When true, log tenant routing for semantic find and session writes (messages/commit) to the plugin logger. */
logFindRequests?: boolean;
// SCCS integration (tool-output compression)
sccsEnabled?: boolean;
sccsCompressThreshold?: number;
sccsSummaryMaxChars?: number;
sccsEnableSmartSummary?: boolean;
sccsStorageTtlSeconds?: number;
sccsStorageDir?: string;
sccsMaxEntries?: number;

};

const DEFAULT_BASE_URL = "http://127.0.0.1:1933";
Expand All @@ -58,6 +67,13 @@ const DEFAULT_INGEST_REPLY_ASSIST_MIN_CHARS = 120;
const DEFAULT_INGEST_REPLY_ASSIST_IGNORE_SESSION_PATTERNS: string[] = [];
const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false;
const DEFAULT_LOCAL_CONFIG_PATH = join(homedir(), ".openviking", "ov.conf");
const DEFAULT_SCCS_ENABLED = false;
const DEFAULT_SCCS_COMPRESS_THRESHOLD = 3000;
const DEFAULT_SCCS_SUMMARY_MAX_CHARS = 300;
const DEFAULT_SCCS_ENABLE_SMART_SUMMARY = true;
const DEFAULT_SCCS_STORAGE_TTL_SECONDS = 86400;
const DEFAULT_SCCS_STORAGE_DIR = join(homedir(), ".openclaw", "sccs");
const DEFAULT_SCCS_MAX_ENTRIES = 10000;

const DEFAULT_AGENT_ID = "default";

Expand Down Expand Up @@ -167,6 +183,13 @@ export const memoryOpenVikingConfigSchema = {
"ingestReplyAssistIgnoreSessionPatterns",
"emitStandardDiagnostics",
"logFindRequests",
"sccsEnabled",
"sccsCompressThreshold",
"sccsSummaryMaxChars",
"sccsEnableSmartSummary",
"sccsStorageTtlSeconds",
"sccsStorageDir",
"sccsMaxEntries",
],
"openviking config",
);
Expand Down Expand Up @@ -270,6 +293,34 @@ export const memoryOpenVikingConfigSchema = {
cfg.logFindRequests === true ||
envFlag("OPENVIKING_LOG_ROUTING") ||
envFlag("OPENVIKING_DEBUG"),
sccsEnabled: cfg.sccsEnabled === true ? true : DEFAULT_SCCS_ENABLED,
sccsCompressThreshold: Math.max(
2000,
Math.floor(toNumber(cfg.sccsCompressThreshold, DEFAULT_SCCS_COMPRESS_THRESHOLD)),
),
sccsSummaryMaxChars: Math.max(
50,
Math.floor(toNumber(cfg.sccsSummaryMaxChars, DEFAULT_SCCS_SUMMARY_MAX_CHARS)),
),
sccsEnableSmartSummary:
typeof cfg.sccsEnableSmartSummary === "boolean"
? cfg.sccsEnableSmartSummary
: DEFAULT_SCCS_ENABLE_SMART_SUMMARY,
sccsStorageTtlSeconds: Math.max(
600,
Math.floor(toNumber(cfg.sccsStorageTtlSeconds, DEFAULT_SCCS_STORAGE_TTL_SECONDS)),
),
sccsStorageDir: resolvePath(
resolveEnvVars(
typeof cfg.sccsStorageDir === "string" && cfg.sccsStorageDir.trim()
? cfg.sccsStorageDir
: DEFAULT_SCCS_STORAGE_DIR,
).replace(/^~/, homedir()),
),
sccsMaxEntries: Math.max(
1000,
Math.floor(toNumber(cfg.sccsMaxEntries, DEFAULT_SCCS_MAX_ENTRIES)),
),
};
},
uiHints: {
Expand Down
25 changes: 23 additions & 2 deletions examples/openclaw-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
openClawSessionToOvStorageId,
} from "./context-engine.js";
import type { ContextEngineWithCommit } from "./context-engine.js";
import { createSccsIntegration } from "./sccs/integration.js";

type PluginLogger = {
debug?: (message: string) => void;
Expand Down Expand Up @@ -527,6 +528,18 @@ const contextEnginePlugin = {
? (api.pluginConfig as Record<string, unknown>)
: {};
const cfg = memoryOpenVikingConfigSchema.parse(api.pluginConfig);
const sccs = createSccsIntegration({
cfg: {
enabled: cfg.sccsEnabled,
compressThreshold: cfg.sccsCompressThreshold,
summaryMaxChars: cfg.sccsSummaryMaxChars,
enableSmartSummary: cfg.sccsEnableSmartSummary,
storageTtlSeconds: cfg.sccsStorageTtlSeconds,
storageDir: cfg.sccsStorageDir,
maxEntries: cfg.sccsMaxEntries,
},
logger: api.logger,
});
const bypassSessionPatterns = compileSessionPatterns(cfg.bypassSessionPatterns);
const rawAgentId = rawCfg.agentId;
if (cfg.logFindRequests) {
Expand Down Expand Up @@ -1062,6 +1075,10 @@ const mergeFindResults = (results: FindResult[]): FindResult => {
{ name: "memory_recall" },
);

if (sccs.enabled && sccs.tool) {
api.registerTool(sccs.tool, { name: "fetch_original_data" });
}

api.registerTool(
(ctx: ToolContext) => ({
name: "memory_store",
Expand Down Expand Up @@ -1589,7 +1606,7 @@ const mergeFindResults = (results: FindResult[]): FindResult => {

if (typeof api.registerContextEngine === "function") {
api.registerContextEngine(contextEnginePlugin.id, () => {
contextEngineRef = createMemoryOpenVikingContextEngine({
const baseEngine = createMemoryOpenVikingContextEngine({
id: contextEnginePlugin.id,
name: contextEnginePlugin.name,
version: "0.1.0",
Expand All @@ -1603,10 +1620,14 @@ const mergeFindResults = (results: FindResult[]): FindResult => {
resolveAgentId,
rememberSessionAgentId,
});
// Wrap base engine with SCCS compression layer (no-op when sccsEnabled=false).
// Cast: spread preserves all base engine props (including commitOVSession),
// but TS cannot infer the subtype relationship across the internal ContextEngine types.
contextEngineRef = sccs.wrapContextEngine(baseEngine) as typeof contextEngineRef;
return contextEngineRef;
});
api.logger.info(
"openviking: registered context-engine (before_prompt_build=auto-recall, afterTurn=auto-capture, assemble=archive+active, session→OV id=uuid-or-sha256 + diag/Phase2 options)",
`openviking: registered context-engine (before_prompt_build=auto-recall, afterTurn=auto-capture, assemble=archive+active, session→OV id=uuid-or-sha256 + diag/Phase2 options)${sccs.enabled ? " + SCCS compression" : ""}`,
);
} else {
api.logger.warn(
Expand Down
49 changes: 49 additions & 0 deletions examples/openclaw-plugin/openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,34 @@
"label": "Log find requests",
"help": "Log tenant routing: /search/find + session messages/commit (X-OpenViking-*; not apiKey). Or set env OPENVIKING_LOG_ROUTING=1 or OPENVIKING_DEBUG=1. Local mode: subprocess stderr at info when enabled.",
"advanced": true
},
"sccsEnabled": {
"label": "Enable SCCS Compression",
"help": "Enable SCCS tool-output compression in the context engine"
},
"sccsCompressThreshold": {
"label": "SCCS Compress Threshold",
"help": "Compress tool outputs longer than this many characters"
},
"sccsSummaryMaxChars": {
"label": "SCCS Summary Max Chars",
"help": "Maximum characters for SCCS REF_ID summaries"
},
"sccsEnableSmartSummary": {
"label": "SCCS Smart Summary",
"help": "Enable structured summary extraction for SCCS"
},
"sccsStorageTtlSeconds": {
"label": "SCCS Storage TTL (seconds)",
"help": "How long REF_ID content is retained"
},
"sccsStorageDir": {
"label": "SCCS Storage Directory",
"help": "Directory for persisted REF_ID content (default: ~/.openclaw/sccs)"
},
"sccsMaxEntries": {
"label": "SCCS Max Entries",
"help": "Max in-memory REF_ID entries before eviction"
}
},
"configSchema": {
Expand Down Expand Up @@ -220,6 +248,27 @@
},
"logFindRequests": {
"type": "boolean"
},
"sccsEnabled": {
"type": "boolean"
},
"sccsCompressThreshold": {
"type": "number"
},
"sccsSummaryMaxChars": {
"type": "number"
},
"sccsEnableSmartSummary": {
"type": "boolean"
},
"sccsStorageTtlSeconds": {
"type": "number"
},
"sccsStorageDir": {
"type": "string"
},
"sccsMaxEntries": {
"type": "number"
}
}
}
Expand Down
68 changes: 68 additions & 0 deletions examples/openclaw-plugin/sccs/compressor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { SummaryExtractor } from "./summarizer.js";
import { extractTextContent, hasRefId, isToolRole, md5Hex, setTextContent } from "./utils.js";
import type { RefStore } from "./storage.js";
export type CompressorConfig = {
compressThreshold: number;
summaryMaxChars: number;
enableSmartSummary: boolean;
storageTtlSeconds: number;
};

export const REF_ID_INSTRUCTION = `\n=== REF_ID DECISION GUIDELINES ===\nWhen you see any [REF_ID: xxx] in a tool response:\n1. Read the summary carefully.\n2. Ask yourself: 'Does this summary contain enough information for my current task?'\n- Yes → proceed normally, ignore the REF_ID.\n- No → call fetch_original_data for that REF_ID.\n3. Common cases where you SHOULD fetch:\n- You need more than ~30 lines of code\n- You need exact line numbers/indentation\n- You plan to edit/replace and need full context\n4. Common cases where you can skip:\n- You only needed to confirm a function exists\n- You only care about a small part already in the summary\n`;

const OPENCLAW_CONFIG_WHITELIST = [
"# SOUL.md",
"# MEMORY.md",
"# USER.md",
"# AGENTS.md",
"# HEARTBEAT.md",
"# IDENTIFY.md",
"# TOOLS.md",
"# BOOTSTRAP.md"
];

function isOpenClawConfigFile(text: string): boolean {
const firstLine = text.trim().split('\n')[0]?.trim();
if (!firstLine) return false;
return OPENCLAW_CONFIG_WHITELIST.some(pattern => firstLine.includes(pattern));
}

async function buildSummary(params: { text: string; config: CompressorConfig }): Promise<{ refId: string; summary: string }> {
const summarizer = new SummaryExtractor(params.config.summaryMaxChars);
const summary = summarizer.summarize(params.text, params.config.enableSmartSummary);
return { refId: md5Hex(params.text), summary };
}

export async function compressToolMessages(params: {
messages: Array<{ role?: unknown; content?: unknown }>;
config: CompressorConfig;
store: RefStore;
logger?: { info?: (msg: string) => void; warn?: (msg: string) => void };
}): Promise<{ messages: Array<{ role?: unknown; content?: unknown }>; systemPromptAddition?: string; compressedCount: number }> {
const { messages, config, store, logger } = params;
let compressedCount = 0;
const nextMessages = messages.map((msg) => ({ ...msg }));

for (let i = 0; i < nextMessages.length; i++) {
const msg = nextMessages[i];
if (!isToolRole(msg.role)) continue;
const text = extractTextContent(msg.content);
if (!text) continue;
if (text.length <= config.compressThreshold || hasRefId(text)) continue;
if (isOpenClawConfigFile(text)) {
continue;
}
const summaryResult = await buildSummary({ text, config });
await store.set(summaryResult.refId, text, config.storageTtlSeconds);
const compressed = `[REF_ID: ${summaryResult.refId}] (Summary: ${summaryResult.summary}). NOTE: You can pass this REF_ID directly as a tool parameter.`;
nextMessages[i] = setTextContent(msg, compressed);
compressedCount += 1;
logger?.info?.(`[sccs] compressed tool output #${i} -> REF_ID ${summaryResult.refId.slice(0, 8)}...`);
}

return {
messages: nextMessages,
compressedCount,
systemPromptAddition: compressedCount > 0 ? REF_ID_INSTRUCTION : undefined,
};
}
109 changes: 109 additions & 0 deletions examples/openclaw-plugin/sccs/integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { compressToolMessages } from "./compressor.js";
import { DiskBackedStore } from "./storage.js";
import { estimateTokensForMessages, resolveHomePath } from "./utils.js";
import { createFetchOriginalDataTool } from "./ref-tool.js";

export type SccsConfig = {
enabled: boolean;
compressThreshold: number;
summaryMaxChars: number;
enableSmartSummary: boolean;
storageTtlSeconds: number;
storageDir: string;
maxEntries?: number;
};

type AgentMessage = { role?: string; content?: unknown };

type AssembleResult = {
messages: AgentMessage[];
estimatedTokens: number;
systemPromptAddition?: string;
};

type ContextEngine = {
info: { id: string; name: string; version?: string };
assemble: (params: { sessionId: string; messages: AgentMessage[]; tokenBudget?: number }) => Promise<AssembleResult>;
ingest: (params: { sessionId: string; message: AgentMessage; isHeartbeat?: boolean }) => Promise<{ ingested: boolean }>;
ingestBatch?: (params: { sessionId: string; messages: AgentMessage[]; isHeartbeat?: boolean }) => Promise<{ ingestedCount: number }>;
afterTurn?: (params: {
sessionId: string;
sessionFile: string;
messages: AgentMessage[];
prePromptMessageCount: number;
autoCompactionSummary?: string;
isHeartbeat?: boolean;
tokenBudget?: number;
runtimeContext?: Record<string, unknown>;
}) => Promise<void>;
compact: (params: {
sessionId: string;
sessionFile: string;
tokenBudget?: number;
force?: boolean;
currentTokenCount?: number;
compactionTarget?: "budget" | "threshold";
customInstructions?: string;
runtimeContext?: Record<string, unknown>;
}) => Promise<{ ok: boolean; compacted: boolean; reason?: string; result?: unknown }>;
};

type Logger = { info: (msg: string) => void; warn?: (msg: string) => void };

export function createSccsIntegration(params: { cfg: SccsConfig; logger: Logger }) {
if (!params.cfg.enabled) {
return {
enabled: false as const,
wrapContextEngine: <T extends ContextEngine>(engine: T): T => engine,
tool: undefined,
};
}

const store = new DiskBackedStore({
dir: resolveHomePath(params.cfg.storageDir),
maxEntries: params.cfg.maxEntries,
});

const wrapContextEngine = <T extends ContextEngine>(engine: T): T => {
return {
...engine,
assemble: async (assembleParams) => {
const base = await engine.assemble(assembleParams);
const compressed = await compressToolMessages({
messages: base.messages,
config: {
compressThreshold: params.cfg.compressThreshold,
summaryMaxChars: params.cfg.summaryMaxChars,
enableSmartSummary: params.cfg.enableSmartSummary,
storageTtlSeconds: params.cfg.storageTtlSeconds,
},
store,
logger: params.logger,
});

const systemPromptAddition =
base.systemPromptAddition && compressed.systemPromptAddition
? `${base.systemPromptAddition}\n\n${compressed.systemPromptAddition}`
: base.systemPromptAddition || compressed.systemPromptAddition;

return {
...base,
messages: compressed.messages,
estimatedTokens: estimateTokensForMessages(compressed.messages),
...(systemPromptAddition ? { systemPromptAddition } : {}),
};
},
};
};

const tool = createFetchOriginalDataTool({
store,
logger: params.logger,
});

return {
enabled: true as const,
wrapContextEngine,
tool,
};
}
Loading