@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 +29 -2
- package/package.json +34 -8
- package/src/compression.ts +176 -0
- package/src/index.ts +106 -18
package/README.md
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
# context-capsule — OpenClaw ContextEngine plugin
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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": "
|
|
4
|
-
"description": "context
|
|
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": [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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": {
|
|
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
|
-
//
|
|
19
|
-
|
|
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: "
|
|
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
|
-
//
|
|
147
|
-
|
|
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(
|
|
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 =
|
|
156
|
-
const older =
|
|
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(
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"
|
|
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
|
},
|