@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 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.6",
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
- return Math.floor(this.config.incrementalMaxDepth);
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
- return {
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,