@martian-engineering/lossless-claw 0.1.0 → 0.1.2
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 +23 -3
- package/docs/configuration.md +8 -2
- package/index.ts +43 -15
- package/openclaw.plugin.json +62 -0
- package/package.json +2 -1
- package/src/db/config.ts +6 -0
- package/src/engine.ts +2 -5
- package/src/summarize.ts +0 -2
package/README.md
CHANGED
|
@@ -26,12 +26,17 @@ Nothing is lost. Raw messages stay in the database. Summaries link back to their
|
|
|
26
26
|
|
|
27
27
|
### Install the plugin
|
|
28
28
|
|
|
29
|
+
**From npm** (recommended):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @martian-engineering/lossless-claw
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**From source** (for development):
|
|
36
|
+
|
|
29
37
|
```bash
|
|
30
|
-
# Clone the repo
|
|
31
38
|
git clone https://github.com/Martian-Engineering/lossless-claw.git
|
|
32
39
|
cd lossless-claw
|
|
33
|
-
|
|
34
|
-
# Install dependencies
|
|
35
40
|
npm install
|
|
36
41
|
```
|
|
37
42
|
|
|
@@ -39,6 +44,21 @@ npm install
|
|
|
39
44
|
|
|
40
45
|
Add the plugin to your OpenClaw config (`~/.openclaw/openclaw.json`):
|
|
41
46
|
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"plugins": {
|
|
50
|
+
"paths": [
|
|
51
|
+
"node_modules/@martian-engineering/lossless-claw"
|
|
52
|
+
],
|
|
53
|
+
"slots": {
|
|
54
|
+
"contextEngine": "lossless-claw"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If installed from source, use the absolute path to the cloned repo instead:
|
|
61
|
+
|
|
42
62
|
```json
|
|
43
63
|
{
|
|
44
64
|
"plugins": {
|
package/docs/configuration.md
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Quick start
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Install the plugin and add it to your OpenClaw config:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @martian-engineering/lossless-claw
|
|
9
|
+
```
|
|
6
10
|
|
|
7
11
|
```json
|
|
8
12
|
{
|
|
9
13
|
"plugins": {
|
|
10
|
-
"paths": ["/
|
|
14
|
+
"paths": ["node_modules/@martian-engineering/lossless-claw"],
|
|
11
15
|
"slots": {
|
|
12
16
|
"contextEngine": "lossless-claw"
|
|
13
17
|
}
|
|
@@ -15,6 +19,8 @@ Add LCM to your OpenClaw config:
|
|
|
15
19
|
}
|
|
16
20
|
```
|
|
17
21
|
|
|
22
|
+
If installed from source, use the absolute path to the repo instead of `node_modules/...`.
|
|
23
|
+
|
|
18
24
|
Set recommended environment variables:
|
|
19
25
|
|
|
20
26
|
```bash
|
package/index.ts
CHANGED
|
@@ -39,8 +39,29 @@ function normalizeAgentId(agentId: string | undefined): string {
|
|
|
39
39
|
return normalized.length > 0 ? normalized : "main";
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
type PluginEnvSnapshot = {
|
|
43
|
+
lcmSummaryModel: string;
|
|
44
|
+
lcmSummaryProvider: string;
|
|
45
|
+
openclawProvider: string;
|
|
46
|
+
agentDir: string;
|
|
47
|
+
home: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type ReadEnvFn = (key: string) => string | undefined;
|
|
51
|
+
|
|
52
|
+
/** Capture plugin env values once during initialization. */
|
|
53
|
+
function snapshotPluginEnv(env: NodeJS.ProcessEnv = process.env): PluginEnvSnapshot {
|
|
54
|
+
return {
|
|
55
|
+
lcmSummaryModel: env.LCM_SUMMARY_MODEL?.trim() ?? "",
|
|
56
|
+
lcmSummaryProvider: env.LCM_SUMMARY_PROVIDER?.trim() ?? "",
|
|
57
|
+
openclawProvider: env.OPENCLAW_PROVIDER?.trim() ?? "",
|
|
58
|
+
agentDir: env.OPENCLAW_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || "",
|
|
59
|
+
home: env.HOME?.trim() ?? "",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
/** Resolve common provider API keys from environment. */
|
|
43
|
-
function resolveApiKey(provider: string): string | undefined {
|
|
64
|
+
function resolveApiKey(provider: string, readEnv: ReadEnvFn): string | undefined {
|
|
44
65
|
const keyMap: Record<string, string[]> = {
|
|
45
66
|
openai: ["OPENAI_API_KEY"],
|
|
46
67
|
anthropic: ["ANTHROPIC_API_KEY"],
|
|
@@ -59,7 +80,7 @@ function resolveApiKey(provider: string): string | undefined {
|
|
|
59
80
|
keys.push(normalizedProviderEnv);
|
|
60
81
|
|
|
61
82
|
for (const key of keys) {
|
|
62
|
-
const value =
|
|
83
|
+
const value = readEnv(key)?.trim();
|
|
63
84
|
if (value) {
|
|
64
85
|
return value;
|
|
65
86
|
}
|
|
@@ -255,19 +276,19 @@ function mergeAuthProfileStores(stores: AuthProfileStore[]): AuthProfileStore |
|
|
|
255
276
|
}
|
|
256
277
|
|
|
257
278
|
/** Determine candidate auth store paths ordered by precedence. */
|
|
258
|
-
function resolveAuthStorePaths(agentDir?: string): string[] {
|
|
279
|
+
function resolveAuthStorePaths(params: { agentDir?: string; envSnapshot: PluginEnvSnapshot }): string[] {
|
|
259
280
|
const paths: string[] = [];
|
|
260
|
-
const directAgentDir = agentDir?.trim();
|
|
281
|
+
const directAgentDir = params.agentDir?.trim();
|
|
261
282
|
if (directAgentDir) {
|
|
262
283
|
paths.push(join(directAgentDir, "auth-profiles.json"));
|
|
263
284
|
}
|
|
264
285
|
|
|
265
|
-
const envAgentDir =
|
|
286
|
+
const envAgentDir = params.envSnapshot.agentDir;
|
|
266
287
|
if (envAgentDir) {
|
|
267
288
|
paths.push(join(envAgentDir, "auth-profiles.json"));
|
|
268
289
|
}
|
|
269
290
|
|
|
270
|
-
const home =
|
|
291
|
+
const home = params.envSnapshot.home;
|
|
271
292
|
if (home) {
|
|
272
293
|
paths.push(join(home, ".openclaw", "agents", "main", "agent", "auth-profiles.json"));
|
|
273
294
|
}
|
|
@@ -334,8 +355,12 @@ async function resolveApiKeyFromAuthProfiles(params: {
|
|
|
334
355
|
agentDir?: string;
|
|
335
356
|
runtimeConfig?: unknown;
|
|
336
357
|
piAiModule: PiAiModule;
|
|
358
|
+
envSnapshot: PluginEnvSnapshot;
|
|
337
359
|
}): Promise<string | undefined> {
|
|
338
|
-
const storesWithPaths = resolveAuthStorePaths(
|
|
360
|
+
const storesWithPaths = resolveAuthStorePaths({
|
|
361
|
+
agentDir: params.agentDir,
|
|
362
|
+
envSnapshot: params.envSnapshot,
|
|
363
|
+
})
|
|
339
364
|
.map((path) => {
|
|
340
365
|
try {
|
|
341
366
|
const parsed = parseAuthProfileStore(readFileSync(path, "utf8"));
|
|
@@ -526,7 +551,9 @@ function readLatestAssistantReply(messages: unknown[]): string | undefined {
|
|
|
526
551
|
|
|
527
552
|
/** Construct LCM dependencies from plugin API/runtime surfaces. */
|
|
528
553
|
function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
529
|
-
const
|
|
554
|
+
const envSnapshot = snapshotPluginEnv();
|
|
555
|
+
const readEnv: ReadEnvFn = (key) => process.env[key];
|
|
556
|
+
const config = resolveLcmConfig();
|
|
530
557
|
|
|
531
558
|
return {
|
|
532
559
|
config,
|
|
@@ -602,7 +629,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
602
629
|
maxTokens: 8_000,
|
|
603
630
|
};
|
|
604
631
|
|
|
605
|
-
let resolvedApiKey = apiKey?.trim() || resolveApiKey(providerId);
|
|
632
|
+
let resolvedApiKey = apiKey?.trim() || resolveApiKey(providerId, readEnv);
|
|
606
633
|
if (!resolvedApiKey && typeof mod.getEnvApiKey === "function") {
|
|
607
634
|
resolvedApiKey = mod.getEnvApiKey(providerId)?.trim();
|
|
608
635
|
}
|
|
@@ -613,6 +640,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
613
640
|
agentDir,
|
|
614
641
|
runtimeConfig,
|
|
615
642
|
piAiModule: mod,
|
|
643
|
+
envSnapshot,
|
|
616
644
|
});
|
|
617
645
|
}
|
|
618
646
|
|
|
@@ -673,7 +701,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
673
701
|
}
|
|
674
702
|
},
|
|
675
703
|
resolveModel: (modelRef, providerHint) => {
|
|
676
|
-
const raw = (modelRef ??
|
|
704
|
+
const raw = (modelRef ?? envSnapshot.lcmSummaryModel).trim();
|
|
677
705
|
if (!raw) {
|
|
678
706
|
throw new Error("No model configured for LCM summarization.");
|
|
679
707
|
}
|
|
@@ -688,15 +716,15 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
688
716
|
|
|
689
717
|
const provider = (
|
|
690
718
|
providerHint?.trim() ||
|
|
691
|
-
|
|
692
|
-
|
|
719
|
+
envSnapshot.lcmSummaryProvider ||
|
|
720
|
+
envSnapshot.openclawProvider ||
|
|
693
721
|
"openai"
|
|
694
722
|
).trim();
|
|
695
723
|
return { provider, model: raw };
|
|
696
724
|
},
|
|
697
|
-
getApiKey: (provider) => resolveApiKey(provider),
|
|
725
|
+
getApiKey: (provider) => resolveApiKey(provider, readEnv),
|
|
698
726
|
requireApiKey: (provider) => {
|
|
699
|
-
const key = resolveApiKey(provider);
|
|
727
|
+
const key = resolveApiKey(provider, readEnv);
|
|
700
728
|
if (!key) {
|
|
701
729
|
throw new Error(`Missing API key for provider '${provider}'.`);
|
|
702
730
|
}
|
|
@@ -756,7 +784,7 @@ const lcmPlugin = {
|
|
|
756
784
|
? (value as Record<string, unknown>)
|
|
757
785
|
: {};
|
|
758
786
|
const enabled = typeof raw.enabled === "boolean" ? raw.enabled : undefined;
|
|
759
|
-
const config = resolveLcmConfig(
|
|
787
|
+
const config = resolveLcmConfig();
|
|
760
788
|
if (enabled !== undefined) {
|
|
761
789
|
config.enabled = enabled;
|
|
762
790
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "lossless-claw",
|
|
3
|
+
"uiHints": {
|
|
4
|
+
"contextThreshold": {
|
|
5
|
+
"label": "Context Threshold",
|
|
6
|
+
"help": "Fraction of context window that triggers compaction (0.0–1.0)"
|
|
7
|
+
},
|
|
8
|
+
"incrementalMaxDepth": {
|
|
9
|
+
"label": "Incremental Max Depth",
|
|
10
|
+
"help": "How deep incremental compaction goes (0 = leaf only)"
|
|
11
|
+
},
|
|
12
|
+
"freshTailCount": {
|
|
13
|
+
"label": "Fresh Tail Count",
|
|
14
|
+
"help": "Number of recent messages protected from compaction"
|
|
15
|
+
},
|
|
16
|
+
"dbPath": {
|
|
17
|
+
"label": "Database Path",
|
|
18
|
+
"help": "Path to LCM SQLite database (default: ~/.openclaw/lcm.db)"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"configSchema": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"properties": {
|
|
25
|
+
"enabled": {
|
|
26
|
+
"type": "boolean"
|
|
27
|
+
},
|
|
28
|
+
"contextThreshold": {
|
|
29
|
+
"type": "number",
|
|
30
|
+
"minimum": 0,
|
|
31
|
+
"maximum": 1
|
|
32
|
+
},
|
|
33
|
+
"incrementalMaxDepth": {
|
|
34
|
+
"type": "integer",
|
|
35
|
+
"minimum": 0
|
|
36
|
+
},
|
|
37
|
+
"freshTailCount": {
|
|
38
|
+
"type": "integer",
|
|
39
|
+
"minimum": 1
|
|
40
|
+
},
|
|
41
|
+
"leafMinFanout": {
|
|
42
|
+
"type": "integer",
|
|
43
|
+
"minimum": 2
|
|
44
|
+
},
|
|
45
|
+
"condensedMinFanout": {
|
|
46
|
+
"type": "integer",
|
|
47
|
+
"minimum": 2
|
|
48
|
+
},
|
|
49
|
+
"condensedMinFanoutHard": {
|
|
50
|
+
"type": "integer",
|
|
51
|
+
"minimum": 2
|
|
52
|
+
},
|
|
53
|
+
"dbPath": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"largeFileThresholdTokens": {
|
|
57
|
+
"type": "integer",
|
|
58
|
+
"minimum": 1000
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martian-engineering/lossless-claw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"index.ts",
|
|
20
20
|
"src/**/*.ts",
|
|
21
|
+
"openclaw.plugin.json",
|
|
21
22
|
"docs/",
|
|
22
23
|
"README.md",
|
|
23
24
|
"LICENSE"
|
package/src/db/config.ts
CHANGED
|
@@ -15,6 +15,10 @@ export type LcmConfig = {
|
|
|
15
15
|
condensedTargetTokens: number;
|
|
16
16
|
maxExpandTokens: number;
|
|
17
17
|
largeFileTokenThreshold: number;
|
|
18
|
+
/** Provider override for large-file text summarization. */
|
|
19
|
+
largeFileSummaryProvider: string;
|
|
20
|
+
/** Model override for large-file text summarization. */
|
|
21
|
+
largeFileSummaryModel: string;
|
|
18
22
|
autocompactDisabled: boolean;
|
|
19
23
|
/** IANA timezone for timestamps in summaries (from TZ env or system default) */
|
|
20
24
|
timezone: string;
|
|
@@ -37,6 +41,8 @@ export function resolveLcmConfig(env: NodeJS.ProcessEnv = process.env): LcmConfi
|
|
|
37
41
|
condensedTargetTokens: parseInt(env.LCM_CONDENSED_TARGET_TOKENS ?? "2000", 10),
|
|
38
42
|
maxExpandTokens: parseInt(env.LCM_MAX_EXPAND_TOKENS ?? "4000", 10),
|
|
39
43
|
largeFileTokenThreshold: parseInt(env.LCM_LARGE_FILE_TOKEN_THRESHOLD ?? "25000", 10),
|
|
44
|
+
largeFileSummaryProvider: env.LCM_LARGE_FILE_SUMMARY_PROVIDER?.trim() ?? "",
|
|
45
|
+
largeFileSummaryModel: env.LCM_LARGE_FILE_SUMMARY_MODEL?.trim() ?? "",
|
|
40
46
|
autocompactDisabled: env.LCM_AUTOCOMPACT_DISABLED === "true",
|
|
41
47
|
timezone: env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
42
48
|
pruneHeartbeatOk: env.LCM_PRUNE_HEARTBEAT_OK === "true",
|
package/src/engine.ts
CHANGED
|
@@ -647,9 +647,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
647
647
|
customInstructions?: string;
|
|
648
648
|
}): Promise<(text: string, aggressive?: boolean) => Promise<string>> {
|
|
649
649
|
const lp = params.legacyParams ?? {};
|
|
650
|
-
console.error(`[lcm] resolveSummarize called, legacyParams keys: ${Object.keys(lp).join(",")}, has summarize fn: ${typeof lp.summarize === "function"}`);
|
|
651
650
|
if (typeof lp.summarize === "function") {
|
|
652
|
-
console.error(`[lcm] resolveSummarize: using legacy summarize function`);
|
|
653
651
|
return lp.summarize as (text: string, aggressive?: boolean) => Promise<string>;
|
|
654
652
|
}
|
|
655
653
|
try {
|
|
@@ -659,7 +657,6 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
659
657
|
customInstructions: params.customInstructions,
|
|
660
658
|
});
|
|
661
659
|
if (runtimeSummarizer) {
|
|
662
|
-
console.error(`[lcm] resolveSummarize: got runtime summarizer`);
|
|
663
660
|
return runtimeSummarizer;
|
|
664
661
|
}
|
|
665
662
|
console.error(`[lcm] resolveSummarize: createLcmSummarizeFromLegacyParams returned undefined`);
|
|
@@ -684,8 +681,8 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
684
681
|
}
|
|
685
682
|
this.largeFileTextSummarizerResolved = true;
|
|
686
683
|
|
|
687
|
-
const provider =
|
|
688
|
-
const model =
|
|
684
|
+
const provider = this.deps.config.largeFileSummaryProvider;
|
|
685
|
+
const model = this.deps.config.largeFileSummaryModel;
|
|
689
686
|
if (!provider || !model) {
|
|
690
687
|
return undefined;
|
|
691
688
|
}
|
package/src/summarize.ts
CHANGED
|
@@ -338,12 +338,10 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
338
338
|
const modelHint =
|
|
339
339
|
typeof params.legacyParams.model === "string" ? params.legacyParams.model.trim() : "";
|
|
340
340
|
const modelRef = modelHint || undefined;
|
|
341
|
-
console.error(`[lcm] createLcmSummarize: providerHint="${providerHint}", modelHint="${modelHint}", modelRef="${modelRef}"`);
|
|
342
341
|
|
|
343
342
|
let resolved: { provider: string; model: string };
|
|
344
343
|
try {
|
|
345
344
|
resolved = params.deps.resolveModel(modelRef, providerHint || undefined);
|
|
346
|
-
console.error(`[lcm] createLcmSummarize: resolved model=${resolved.model}, provider=${resolved.provider}`);
|
|
347
345
|
} catch (err) {
|
|
348
346
|
console.error(`[lcm] createLcmSummarize: resolveModel FAILED:`, err instanceof Error ? err.message : err);
|
|
349
347
|
return undefined;
|