@parad0x_labs/openclaw-context-capsule 0.1.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,39 @@
1
1
  # context-capsule — OpenClaw ContextEngine plugin
2
2
 
3
- Compresses agent session history before it reaches the LLM using
4
- [`@parad0x_labs/context-capsule`](https://github.com/Parad0x-Labs/dna-x402/tree/main/packages/context-capsule).
3
+ > 💜 **Saving you tokens?** [Star it on ClawHub](https://clawhub.ai/parad0x-labs/context-capsule) — it's a context engine, so it works silently in the background. A star is the only way other agent builders find it.
4
+
5
+ Compresses agent session history before it reaches the LLM. **Self-contained:**
6
+ the compression core is vendored inline (`src/compression.ts`) — no external
7
+ runtime dependency, and no network, file-system, or on-chain access. It uses
8
+ only Node's built-in `zlib` and `crypto`.
9
+
5
10
  Sessions under 20 messages pass through unchanged. Longer sessions have their
6
11
  older history compressed into a capsule summary (injected as a system message)
7
12
  while the last 10 messages are kept verbatim — giving the model full coherence
8
13
  on recent turns without paying for the full transcript.
9
14
 
15
+ > **Before you use this skill — read these:**
16
+ >
17
+ > - **All messages are vault-scanned** for secrets and PII on every path (short
18
+ > sessions, verbatim tail, and compressed history alike). Matched values are
19
+ > replaced with typed placeholders. However, the vault scan covers common
20
+ > patterns — it is not a guarantee that all sensitive content is removed.
21
+ > Do not rely on it as the sole protection for highly sensitive sessions.
22
+ >
23
+ > - **Compression alters history fidelity.** Older messages are summarised, not
24
+ > preserved verbatim. Detail, nuance, and exact wording can be lost. Do not
25
+ > use this skill where exact transcript fidelity is required.
26
+ >
27
+ > - **Compressed history is injected as a system message.** This places
28
+ > summarised content in a privileged prompt position. Be aware that prior
29
+ > user/assistant content will influence the model from the system role after
30
+ > compression.
31
+ >
32
+ > - **No external runtime dependency.** The compression core is vendored inline
33
+ > (`src/compression.ts`), so there is nothing external to resolve or verify.
34
+ > The standalone `@parad0x_labs/context-capsule` library on npm is optional and
35
+ > only relevant for non-OpenClaw use.
36
+
10
37
  **Most useful for:** local models (Ollama, LM Studio) and GPT-4 where context
11
38
  cost matters. Claude users with a 200k context window and built-in compaction
12
39
  enabled may not need this.
package/package.json CHANGED
@@ -1,14 +1,40 @@
1
1
  {
2
2
  "name": "@parad0x_labs/openclaw-context-capsule",
3
- "version": "0.1.0",
4
- "description": "context-capsule ContextEngine for OpenClaw 99.3% token reduction with recovery-score gate. Works with Ollama, GPT, Mistral, Claude.",
3
+ "version": "1.4.0",
4
+ "description": "Self-contained OpenClaw context engine that compresses long agent session history (> 20 messages) into a compact capsule before the model call, keeping the last 10 messages verbatim. Cuts token cost on long chats with any model. No external runtime dependencies; no network, file, or on-chain access.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
7
- "files": ["src/", "openclaw.plugin.json", "README.md"],
8
- "keywords": ["openclaw", "openclaw-plugin", "context-compression", "llm", "token-savings", "ollama", "context-capsule"],
9
- "openclaw": { "extensions": ["./src/index.ts"] },
10
- "dependencies": { "@parad0x_labs/context-capsule": "^0.1.0" },
11
- "engines": { "node": ">=22" },
7
+ "files": [
8
+ "src/",
9
+ "openclaw.plugin.json",
10
+ "README.md"
11
+ ],
12
+ "keywords": [
13
+ "openclaw",
14
+ "openclaw-plugin",
15
+ "context-compression",
16
+ "llm",
17
+ "token-savings",
18
+ "ollama",
19
+ "context-capsule"
20
+ ],
21
+ "openclaw": {
22
+ "extensions": [
23
+ "./src/index.ts"
24
+ ],
25
+ "compat": {
26
+ "pluginApi": "1.x"
27
+ },
28
+ "build": {
29
+ "openclawVersion": ">=1.0.0"
30
+ }
31
+ },
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
12
35
  "license": "MIT",
13
- "repository": { "type": "git", "url": "https://github.com/Parad0x-Labs/dna-x402" }
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/Parad0x-Labs/dna-x402"
39
+ }
14
40
  }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Self-contained compression core for the Context Capsule skill.
3
+ *
4
+ * This is a VENDORED, MINIMAL SUBSET of @parad0x_labs/context-capsule —
5
+ * only the two pure functions this plugin actually uses (compressContext +
6
+ * injectCapsule) and their local helpers. It is bundled directly in the skill
7
+ * so the published artifact is fully self-contained and auditable: there is no
8
+ * external runtime dependency, and nothing here performs network I/O, file
9
+ * access, dynamic imports, or on-chain anchoring.
10
+ *
11
+ * Dependencies: Node.js built-ins only — `node:zlib` (deflate) and
12
+ * `node:crypto` (SHA-256). No third-party packages.
13
+ *
14
+ * Provenance: faithfully copied from
15
+ * github.com/Parad0x-Labs/dna-x402/tree/main/packages/context-capsule (MIT)
16
+ * Excluded on purpose: searchCapsule, estimateSavings, intent tagging, and
17
+ * anchorCorrectionChain (the latter is the only code path in the upstream
18
+ * package that touches the network — it is intentionally NOT vendored here).
19
+ */
20
+
21
+ import { createHash } from "node:crypto";
22
+ import { deflateSync } from "node:zlib";
23
+
24
+ // ── Types ───────────────────────────────────────────────────────────────────
25
+
26
+ export interface Message {
27
+ role: string;
28
+ content: string;
29
+ }
30
+
31
+ /** A compressed, auditable snapshot of an agent session's message history. */
32
+ export interface ContextCapsule {
33
+ sessionId: string;
34
+ capsuleId: string;
35
+ originalTokenEstimate: number;
36
+ compressedBytes: number;
37
+ compressionRatio: string;
38
+ topics: string[];
39
+ merkleRoot: string;
40
+ createdAt: number;
41
+ compressedBase64: string;
42
+ }
43
+
44
+ export interface CompressOptions {
45
+ sessionId?: string;
46
+ maxOutputTokens?: number;
47
+ }
48
+
49
+ // ── Internal helpers ────────────────────────────────────────────────────────
50
+
51
+ /** SHA-256 of a UTF-8 string → Buffer */
52
+ function sha256(data: string): Buffer {
53
+ return createHash("sha256").update(data, "utf8").digest();
54
+ }
55
+
56
+ /**
57
+ * Build a SHA-256 Merkle root over an ordered list of leaf buffers.
58
+ * Empty → 32-byte zero buffer. Single leaf → that leaf. Odd node duplicates.
59
+ */
60
+ function buildMerkleRoot(leaves: Buffer[]): Buffer {
61
+ if (leaves.length === 0) return Buffer.alloc(32, 0);
62
+ if (leaves.length === 1) return leaves[0];
63
+
64
+ let level: Buffer[] = leaves;
65
+ while (level.length > 1) {
66
+ const next: Buffer[] = [];
67
+ for (let i = 0; i < level.length; i += 2) {
68
+ const left = level[i];
69
+ const right = i + 1 < level.length ? level[i + 1] : level[i]; // duplicate odd
70
+ next.push(createHash("sha256").update(left).update(right).digest());
71
+ }
72
+ level = next;
73
+ }
74
+ return level[0];
75
+ }
76
+
77
+ /** Extract up to `maxTopics` noun-phrase-like tokens from message text. */
78
+ function extractTopics(messages: Message[], maxTopics = 5): string[] {
79
+ const allText = messages.map((m) => m.content).join(" ");
80
+
81
+ const titlePhrases = allText.match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3}\b/g) ?? [];
82
+ const properNouns = allText.match(/\b[A-Z][a-zA-Z]{3,}\b/g) ?? [];
83
+
84
+ const stopWords = new Set([
85
+ "the", "and", "for", "that", "this", "with", "have", "from", "they", "will",
86
+ "been", "were", "what", "when", "where", "which", "while", "about", "above",
87
+ "after", "before", "between", "through", "during", "because", "should",
88
+ ]);
89
+ const plainWords =
90
+ allText
91
+ .toLowerCase()
92
+ .match(/\b[a-z]{7,}\b/g)
93
+ ?.filter((w) => !stopWords.has(w)) ?? [];
94
+
95
+ const seen = new Set<string>();
96
+ const topics: string[] = [];
97
+ for (const raw of [...titlePhrases, ...properNouns, ...plainWords]) {
98
+ const key = raw.toLowerCase();
99
+ if (!seen.has(key)) {
100
+ seen.add(key);
101
+ topics.push(raw);
102
+ if (topics.length >= maxTopics) break;
103
+ }
104
+ }
105
+ return topics;
106
+ }
107
+
108
+ /** Deterministic capsule ID: sha256(sessionId + createdAt + merkleRoot) */
109
+ function buildCapsuleId(sessionId: string, createdAt: number, merkleRoot: string): string {
110
+ return createHash("sha256")
111
+ .update(`${sessionId}:${createdAt}:${merkleRoot}`)
112
+ .digest("hex")
113
+ .slice(0, 32);
114
+ }
115
+
116
+ /** Estimate token count from character length (chars / 4 approximation) */
117
+ function estimateTokens(text: string): number {
118
+ return Math.ceil(text.length / 4);
119
+ }
120
+
121
+ // ── Public API (subset) ──────────────────────────────────────────────────────
122
+
123
+ /**
124
+ * Compress a session's message history into a ContextCapsule.
125
+ * Pure function — zlib deflate + SHA-256 only, no I/O.
126
+ */
127
+ export function compressContext(messages: Message[], opts: CompressOptions = {}): ContextCapsule {
128
+ if (!messages || messages.length === 0) {
129
+ throw new Error("compressContext: messages must be a non-empty array");
130
+ }
131
+
132
+ const sessionId = opts.sessionId ?? `session_${Date.now()}`;
133
+ const createdAt = Date.now();
134
+
135
+ const jsonl = messages.map((m) => JSON.stringify(m)).join("\n");
136
+ const compressed = deflateSync(Buffer.from(jsonl, "utf8"), { level: 9 });
137
+ const compressedBase64 = compressed.toString("base64");
138
+
139
+ const originalTokenEstimate = estimateTokens(jsonl);
140
+ const originalBytes = Buffer.byteLength(jsonl, "utf8");
141
+ const compressedBytes = compressed.length;
142
+ const compressionRatio = `${(originalBytes / compressedBytes).toFixed(1)}x`;
143
+
144
+ const topics = extractTopics(messages);
145
+
146
+ const leaves = messages.map((m) => sha256(JSON.stringify(m)));
147
+ const merkleRoot = buildMerkleRoot(leaves).toString("hex");
148
+ const capsuleId = buildCapsuleId(sessionId, createdAt, merkleRoot);
149
+
150
+ return {
151
+ sessionId,
152
+ capsuleId,
153
+ originalTokenEstimate,
154
+ compressedBytes,
155
+ compressionRatio,
156
+ topics,
157
+ merkleRoot,
158
+ createdAt,
159
+ compressedBase64,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Generate a compact (~60–100 token) injection string that replaces full
165
+ * history in an LLM call. Pure string builder — no I/O.
166
+ */
167
+ export function injectCapsule(capsule: ContextCapsule): string {
168
+ const topicList = capsule.topics.length > 0 ? capsule.topics.join(", ") : "general conversation";
169
+ const rootShort = capsule.merkleRoot.slice(0, 12);
170
+ return (
171
+ `[CONTEXT CAPSULE: session ${capsule.sessionId} compressed ${capsule.compressionRatio}. ` +
172
+ `Key topics: ${topicList}. ` +
173
+ `Merkle: ${rootShort}... ` +
174
+ `Full history available on request.]`
175
+ );
176
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,22 @@
4
4
  * Compresses session history before it reaches the LLM, achieving ~99% token
5
5
  * reduction while keeping a verbatim tail of recent messages for coherence.
6
6
  * Sessions under the minMessages threshold pass through unchanged.
7
+ *
8
+ * Self-contained (v1.4.0):
9
+ * The compression core is vendored inline (./compression.ts) — there is no
10
+ * external runtime dependency. The plugin makes no network requests, no
11
+ * file-system access, and no on-chain calls. Everything runs locally.
12
+ *
13
+ * Data handling:
14
+ * All message content is passed through an inline vault-scan gate before
15
+ * reaching the compression core OR the model on any code path, including
16
+ * short sessions, the verbatim tail, and compression error fallbacks.
17
+ * The gate strips API keys, tokens, credentials, PII, and card numbers,
18
+ * replacing them with typed placeholders. No matched values are logged —
19
+ * only category counts. On compression failure the plugin is fail-closed:
20
+ * vault-scanned (redacted) messages are returned with a visible console.warn.
21
+ * Redaction is best-effort pattern matching, not a guarantee — do not rely on
22
+ * it as sole protection for highly sensitive sessions.
7
23
  */
8
24
 
9
25
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
@@ -15,8 +31,50 @@ import type {
15
31
  IngestResult,
16
32
  } from "openclaw/plugin-sdk/context-engine";
17
33
  import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
18
- // @ts-expect-error external package, types may not be present at build time
19
- import { compressContext, injectCapsule } from "@parad0x_labs/context-capsule";
34
+ // Self-contained vendored compression core no external runtime dependency.
35
+ // Only zlib + SHA-256, no network/file I/O. See ./compression.ts for provenance.
36
+ import { compressContext, injectCapsule } from "./compression";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Inline vault-scan gate (ported from tools/liquefy_redact.py)
40
+ // Strips secrets and PII before any text crosses the compression boundary.
41
+ // Never logs matched values — only category names and counts.
42
+ // ---------------------------------------------------------------------------
43
+
44
+ const VAULT_PATTERNS: Array<{ key: string; re: RegExp; placeholder: string }> = [
45
+ { key: "pem_key", re: /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/gi, placeholder: "[REDACTED_PRIVATE_KEY]" },
46
+ { key: "anthropic", re: /sk-ant-[A-Za-z0-9\-_]{20,}/g, placeholder: "[REDACTED_ANTHROPIC_KEY]" },
47
+ { key: "openai", re: /sk-[A-Za-z0-9]{20,}T3BlbkFJ[A-Za-z0-9]{20,}/g, placeholder: "[REDACTED_OPENAI_KEY]" },
48
+ { key: "generic_sk", re: /\bsk-[A-Za-z0-9]{20,}\b/g, placeholder: "[REDACTED_SK_KEY]" },
49
+ { key: "github", re: /gh[pousr]_[A-Za-z0-9_]{36,}/g, placeholder: "[REDACTED_GITHUB_TOKEN]" },
50
+ { key: "slack", re: /xox[bpras]-[A-Za-z0-9\-]{10,}/g, placeholder: "[REDACTED_SLACK_TOKEN]" },
51
+ { key: "aws", re: /AKIA[0-9A-Z]{16}/g, placeholder: "[REDACTED_AWS_KEY]" },
52
+ { key: "stripe", re: /(?:sk|pk)_(?:test|live)_[A-Za-z0-9]{24,}/g, placeholder: "[REDACTED_STRIPE_KEY]" },
53
+ { key: "jwt", re: /eyJ[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}\.[A-Za-z0-9_\-]{8,}/g, placeholder: "[REDACTED_JWT]" },
54
+ { key: "bearer", re: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, placeholder: "[REDACTED_BEARER]" },
55
+ { key: "credential", re: /(?:password|passwd|secret|token|api[_\-]?key|access[_\-]?key|auth[_\-]?token)\s*[=:]\s*["']?([A-Za-z0-9/+=\-_.]{8,})["']?/gi, placeholder: "[REDACTED_SECRET]" },
56
+ { key: "card", re: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2})[ \-]?\d{4}[ \-]?\d{4}[ \-]?\d{4}\b/g, placeholder: "[REDACTED_CC]" },
57
+ ];
58
+
59
+ function vaultGuard(text: string, label: string): string {
60
+ let result = text;
61
+ const hits: Record<string, number> = {};
62
+ for (const { key, re, placeholder } of VAULT_PATTERNS) {
63
+ re.lastIndex = 0;
64
+ const matches = result.match(re);
65
+ if (matches?.length) {
66
+ hits[key] = (hits[key] ?? 0) + matches.length;
67
+ re.lastIndex = 0;
68
+ result = result.replace(re, placeholder);
69
+ }
70
+ }
71
+ const total = Object.values(hits).reduce((a, b) => a + b, 0);
72
+ if (total > 0) {
73
+ const summary = Object.entries(hits).map(([k, n]) => `${k}x${n}`).join(", ");
74
+ console.warn(`[context-capsule vault] ${label}: redacted ${total} — ${summary}`);
75
+ }
76
+ return result;
77
+ }
20
78
 
21
79
  // ---------------------------------------------------------------------------
22
80
  // Configuration defaults
@@ -107,7 +165,7 @@ class ContextCapsuleEngine implements ContextEngine {
107
165
  readonly info: ContextEngineInfo = {
108
166
  id: "context-capsule",
109
167
  name: "Context Capsule",
110
- version: "0.1.0",
168
+ version: "1.3.0",
111
169
  ownsCompaction: false,
112
170
  turnMaintenanceMode: "background",
113
171
  };
@@ -143,29 +201,53 @@ class ContextCapsuleEngine implements ContextEngine {
143
201
  }): Promise<AssembleResult> {
144
202
  const { messages } = params;
145
203
 
146
- // Short sessions: pass through unchanged
147
- if (messages.length < this.cfg.minMessages) {
204
+ // Vault gate applied to ALL messages on ALL paths — including short sessions
205
+ // and the verbatim tail — so no content reaches the model unscanned.
206
+ // Runs locally, logs category counts only, never matched values.
207
+ const scannedMessages = messages.map((msg) => {
208
+ const raw =
209
+ "content" in msg && typeof (msg as { content: unknown }).content === "string"
210
+ ? (msg as { content: string }).content
211
+ : "";
212
+ if (!raw) return msg;
213
+ const clean = vaultGuard(raw, `msg[${(msg as { role: string }).role ?? "unknown"}]`);
214
+ return clean === raw ? msg : { ...msg, content: clean };
215
+ });
216
+
217
+ // Short sessions: pass through (vault-scanned above)
218
+ if (scannedMessages.length < this.cfg.minMessages) {
148
219
  return {
149
- messages,
150
- estimatedTokens: estimateTokens(messages),
220
+ messages: scannedMessages,
221
+ estimatedTokens: estimateTokens(scannedMessages),
151
222
  };
152
223
  }
153
224
 
154
- // Compress the older history, keep the tail verbatim
155
- const tail = messages.slice(-this.cfg.keepRecentMessages);
156
- const older = messages.slice(0, -this.cfg.keepRecentMessages);
225
+ // Compress the older history, keep the tail verbatim (both already scanned)
226
+ const tail = scannedMessages.slice(-this.cfg.keepRecentMessages);
227
+ const older = scannedMessages.slice(0, -this.cfg.keepRecentMessages);
157
228
  const normalized = normalizeMessages(older);
158
229
 
230
+ // Older messages were already vault-scanned above; pass directly to compression.
231
+ const safeMessages = normalized;
232
+
159
233
  let summaryText: string;
160
234
  try {
161
- const capsule = await compressContext(normalized);
235
+ const capsule = await compressContext(safeMessages);
162
236
  const injected = await injectCapsule(capsule);
163
237
  summaryText = typeof injected === "string" ? injected : JSON.stringify(injected);
164
- } catch {
165
- // Fallback: skip compression, return original messages
238
+ } catch (err) {
239
+ // Compression failed fall back to vault-scanned messages so the vault
240
+ // guarantee is NEVER broken by a compression error (fail-closed, not fail-open).
241
+ // Emit a visible warning so the operator knows compression was skipped.
242
+ console.warn(
243
+ "[context-capsule] Compression failed — falling back to scanned-but-uncompressed messages. " +
244
+ "Vault redaction still applied; no secrets exposed. " +
245
+ "See ./compression.ts for the compression core.",
246
+ err instanceof Error ? err.message : String(err),
247
+ );
166
248
  return {
167
- messages,
168
- estimatedTokens: estimateTokens(messages),
249
+ messages: scannedMessages,
250
+ estimatedTokens: estimateTokens(scannedMessages),
169
251
  promptAuthority: "preassembly_may_overflow",
170
252
  };
171
253
  }
@@ -215,9 +297,15 @@ export default definePluginEntry({
215
297
  id: "context-capsule",
216
298
  name: "Context Capsule",
217
299
  description:
218
- "99.3% token reduction on agent sessions via @parad0x_labs/context-capsule. " +
219
- "Works with Ollama, LM Studio, GPT-4, Mistral, and Claude. " +
220
- "Public benchmark with recovery-score gate in CI.",
300
+ "Context engine that compresses long agent session history (default: > 20 " +
301
+ "messages) into a compact capsule before the model call, keeping the most " +
302
+ "recent 10 messages verbatim. Use it to cut token cost on long-running chat " +
303
+ "sessions with any model (Ollama, LM Studio, GPT, Mistral, Claude). It " +
304
+ "activates only as the registered context engine for sessions past the " +
305
+ "message threshold — it is NOT a command, has no keyword triggers, makes no " +
306
+ "network or file-system calls, and does no on-chain anchoring. Do NOT use it " +
307
+ "where exact verbatim transcript fidelity is required, as older history is " +
308
+ "summarized. Compression and best-effort secret redaction run fully locally.",
221
309
  register(api) {
222
310
  api.registerContextEngine("context-capsule", (_ctx) => new ContextCapsuleEngine());
223
311
  },