@martian-engineering/lossless-claw 0.1.6 → 0.2.1
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 +1 -1
- package/package.json +1 -1
- package/src/assembler.ts +87 -0
- package/src/compaction.ts +3 -4
- package/src/engine.ts +6 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> ⚠️ **Current requirement:** This plugin currently requires a custom OpenClaw build with [PR #22201](https://github.com/openclaw/openclaw/pull/22201) applied until that PR is merged upstream.
|
|
4
4
|
|
|
5
|
-
Lossless Context Management plugin for [OpenClaw](https://github.com/openclaw/openclaw), based on the [LCM paper](https://voltropy.com/LCM). Replaces OpenClaw's built-in sliding-window compaction with a DAG-based summarization system that preserves every message while keeping active context within model token limits.
|
|
5
|
+
Lossless Context Management plugin for [OpenClaw](https://github.com/openclaw/openclaw), based on the [LCM paper](https://papers.voltropy.com/LCM). Replaces OpenClaw's built-in sliding-window compaction with a DAG-based summarization system that preserves every message while keeping active context within model token limits.
|
|
6
6
|
|
|
7
7
|
## What it does
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martian-engineering/lossless-claw",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Lossless Context Management plugin for OpenClaw — DAG-based conversation summarization with incremental compaction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/src/assembler.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface AssembleContextResult {
|
|
|
23
23
|
messages: AgentMessage[];
|
|
24
24
|
/** Total estimated tokens */
|
|
25
25
|
estimatedTokens: number;
|
|
26
|
+
/** Optional dynamic system prompt guidance derived from DAG state */
|
|
27
|
+
systemPromptAddition?: string;
|
|
26
28
|
/** Stats about what was assembled */
|
|
27
29
|
stats: {
|
|
28
30
|
rawMessageCount: number;
|
|
@@ -38,6 +40,77 @@ function estimateTokens(text: string): number {
|
|
|
38
40
|
return Math.ceil(text.length / 4);
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
type SummaryPromptSignal = Pick<SummaryRecord, "kind" | "depth" | "descendantCount">;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build LCM usage guidance for the runtime system prompt.
|
|
47
|
+
*
|
|
48
|
+
* Guidance is emitted only when summaries are present in assembled context.
|
|
49
|
+
* Depth-aware: minimal for shallow compaction, full guidance for deep trees.
|
|
50
|
+
*/
|
|
51
|
+
function buildSystemPromptAddition(summarySignals: SummaryPromptSignal[]): string | undefined {
|
|
52
|
+
if (summarySignals.length === 0) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const maxDepth = summarySignals.reduce((deepest, signal) => Math.max(deepest, signal.depth), 0);
|
|
57
|
+
const condensedCount = summarySignals.filter((signal) => signal.kind === "condensed").length;
|
|
58
|
+
const heavilyCompacted = maxDepth >= 2 || condensedCount >= 2;
|
|
59
|
+
|
|
60
|
+
const sections: string[] = [];
|
|
61
|
+
|
|
62
|
+
// Core recall workflow — always present when summaries exist
|
|
63
|
+
sections.push(
|
|
64
|
+
"## LCM Recall",
|
|
65
|
+
"",
|
|
66
|
+
"Summaries above are compressed context — maps to details, not the details themselves.",
|
|
67
|
+
"",
|
|
68
|
+
"**Recall priority:** LCM tools first, then qmd (for Granola/Limitless/pre-LCM data), then memory_search as last resort.",
|
|
69
|
+
"",
|
|
70
|
+
"**Tool escalation:**",
|
|
71
|
+
"1. `lcm_grep` — search by regex or full-text across messages and summaries",
|
|
72
|
+
"2. `lcm_describe` — inspect a specific summary (cheap, no sub-agent)",
|
|
73
|
+
"3. `lcm_expand_query` — deep recall: spawns bounded sub-agent, expands DAG, returns answer with cited summary IDs (~120s, don't ration it)",
|
|
74
|
+
"",
|
|
75
|
+
"**`lcm_expand_query` usage** — two patterns (always requires `prompt`):",
|
|
76
|
+
"- With IDs: `lcm_expand_query(summaryIds: [\"sum_xxx\"], prompt: \"What config changes were discussed?\")`",
|
|
77
|
+
"- With search: `lcm_expand_query(query: \"database migration\", prompt: \"What strategy was decided?\")`",
|
|
78
|
+
"- Optional: `maxTokens` (default 2000), `conversationId`, `allConversations: true`",
|
|
79
|
+
"",
|
|
80
|
+
"**Summaries include \"Expand for details about:\" footers** listing compressed specifics. Use `lcm_expand_query` with that summary's ID to retrieve them.",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Precision/evidence rules — always present but stronger when heavily compacted
|
|
84
|
+
if (heavilyCompacted) {
|
|
85
|
+
sections.push(
|
|
86
|
+
"",
|
|
87
|
+
"**\u26a0 Deeply compacted context — expand before asserting specifics.**",
|
|
88
|
+
"",
|
|
89
|
+
"Default recall flow for precision work:",
|
|
90
|
+
"1) `lcm_grep` to locate relevant summary/message IDs",
|
|
91
|
+
"2) `lcm_expand_query` with a focused prompt",
|
|
92
|
+
"3) Answer with citations to summary IDs used",
|
|
93
|
+
"",
|
|
94
|
+
"**Uncertainty checklist (run before answering):**",
|
|
95
|
+
"- Am I making exact factual claims from a condensed summary?",
|
|
96
|
+
"- Could compaction have omitted a crucial detail?",
|
|
97
|
+
"- Would this answer fail if the user asks for proof?",
|
|
98
|
+
"",
|
|
99
|
+
"If yes to any \u2192 expand first.",
|
|
100
|
+
"",
|
|
101
|
+
"**Do not guess** exact commands, SHAs, file paths, timestamps, config values, or causal claims from condensed summaries. Expand first or state that you need to expand.",
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
sections.push(
|
|
105
|
+
"",
|
|
106
|
+
"**For precision/evidence questions** (exact commands, SHAs, paths, timestamps, config values, root-cause chains): expand before answering.",
|
|
107
|
+
"Do not guess from condensed summaries — expand first or state uncertainty.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return sections.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
41
114
|
/**
|
|
42
115
|
* Map a DB message role to an AgentMessage role.
|
|
43
116
|
*
|
|
@@ -267,6 +340,8 @@ interface ResolvedItem {
|
|
|
267
340
|
tokens: number;
|
|
268
341
|
/** Whether this came from a raw message (vs. a summary) */
|
|
269
342
|
isMessage: boolean;
|
|
343
|
+
/** Summary metadata used for dynamic system prompt guidance */
|
|
344
|
+
summarySignal?: SummaryPromptSignal;
|
|
270
345
|
}
|
|
271
346
|
|
|
272
347
|
// ── ContextAssembler ─────────────────────────────────────────────────────────
|
|
@@ -309,14 +384,20 @@ export class ContextAssembler {
|
|
|
309
384
|
// Count stats from the full (pre-truncation) set
|
|
310
385
|
let rawMessageCount = 0;
|
|
311
386
|
let summaryCount = 0;
|
|
387
|
+
const summarySignals: SummaryPromptSignal[] = [];
|
|
312
388
|
for (const item of resolved) {
|
|
313
389
|
if (item.isMessage) {
|
|
314
390
|
rawMessageCount++;
|
|
315
391
|
} else {
|
|
316
392
|
summaryCount++;
|
|
393
|
+
if (item.summarySignal) {
|
|
394
|
+
summarySignals.push(item.summarySignal);
|
|
395
|
+
}
|
|
317
396
|
}
|
|
318
397
|
}
|
|
319
398
|
|
|
399
|
+
const systemPromptAddition = buildSystemPromptAddition(summarySignals);
|
|
400
|
+
|
|
320
401
|
// Step 3: Split into evictable prefix and protected fresh tail
|
|
321
402
|
const tailStart = Math.max(0, resolved.length - freshTailCount);
|
|
322
403
|
const freshTail = resolved.slice(tailStart);
|
|
@@ -388,6 +469,7 @@ export class ContextAssembler {
|
|
|
388
469
|
return {
|
|
389
470
|
messages: sanitizeToolUseResultPairing(rawMessages) as AgentMessage[],
|
|
390
471
|
estimatedTokens,
|
|
472
|
+
systemPromptAddition,
|
|
391
473
|
stats: {
|
|
392
474
|
rawMessageCount,
|
|
393
475
|
summaryCount,
|
|
@@ -508,6 +590,11 @@ export class ContextAssembler {
|
|
|
508
590
|
message: { role: "user" as const, content } as AgentMessage,
|
|
509
591
|
tokens,
|
|
510
592
|
isMessage: false,
|
|
593
|
+
summarySignal: {
|
|
594
|
+
kind: summary.kind,
|
|
595
|
+
depth: summary.depth,
|
|
596
|
+
descendantCount: summary.descendantCount,
|
|
597
|
+
},
|
|
511
598
|
};
|
|
512
599
|
}
|
|
513
600
|
}
|
package/src/compaction.ts
CHANGED
|
@@ -795,14 +795,13 @@ export class CompactionEngine {
|
|
|
795
795
|
private resolveIncrementalMaxDepth(): number {
|
|
796
796
|
if (
|
|
797
797
|
typeof this.config.incrementalMaxDepth === "number" &&
|
|
798
|
-
Number.isFinite(this.config.incrementalMaxDepth)
|
|
799
|
-
this.config.incrementalMaxDepth > 0
|
|
798
|
+
Number.isFinite(this.config.incrementalMaxDepth)
|
|
800
799
|
) {
|
|
801
|
-
|
|
800
|
+
if (this.config.incrementalMaxDepth < 0) return Infinity;
|
|
801
|
+
if (this.config.incrementalMaxDepth > 0) return Math.floor(this.config.incrementalMaxDepth);
|
|
802
802
|
}
|
|
803
803
|
return 0;
|
|
804
804
|
}
|
|
805
|
-
|
|
806
805
|
private resolveFanoutForDepth(targetDepth: number, hardTrigger: boolean): number {
|
|
807
806
|
if (hardTrigger) {
|
|
808
807
|
return this.resolveCondensedMinFanoutHard();
|
package/src/engine.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { createLcmSummarizeFromLegacyParams } from "./summarize.js";
|
|
|
41
41
|
import type { LcmDependencies } from "./types.js";
|
|
42
42
|
|
|
43
43
|
type AgentMessage = Parameters<ContextEngine["ingest"]>[0]["message"];
|
|
44
|
+
type AssembleResultWithSystemPrompt = AssembleResult & { systemPromptAddition?: string };
|
|
44
45
|
|
|
45
46
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
46
47
|
|
|
@@ -1269,10 +1270,14 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1269
1270
|
};
|
|
1270
1271
|
}
|
|
1271
1272
|
|
|
1272
|
-
|
|
1273
|
+
const result: AssembleResultWithSystemPrompt = {
|
|
1273
1274
|
messages: assembled.messages,
|
|
1274
1275
|
estimatedTokens: assembled.estimatedTokens,
|
|
1276
|
+
...(assembled.systemPromptAddition
|
|
1277
|
+
? { systemPromptAddition: assembled.systemPromptAddition }
|
|
1278
|
+
: {}),
|
|
1275
1279
|
};
|
|
1280
|
+
return result;
|
|
1276
1281
|
} catch {
|
|
1277
1282
|
return {
|
|
1278
1283
|
messages: params.messages,
|