@martian-engineering/lossless-claw 0.5.2 → 0.6.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 +49 -11
- package/docs/configuration.md +44 -0
- package/openclaw.plugin.json +114 -0
- package/package.json +2 -1
- package/skills/lossless-claw/SKILL.md +33 -0
- package/skills/lossless-claw/references/architecture.md +52 -0
- package/skills/lossless-claw/references/config.md +263 -0
- package/skills/lossless-claw/references/diagnostics.md +79 -0
- package/skills/lossless-claw/references/recall-tools.md +55 -0
- package/skills/lossless-claw/references/session-lifecycle.md +59 -0
- package/src/assembler.ts +321 -34
- package/src/compaction.ts +220 -19
- package/src/db/config.ts +74 -21
- package/src/db/migration.ts +50 -13
- package/src/engine.ts +742 -133
- package/src/plugin/index.ts +156 -73
- package/src/plugin/lcm-command.ts +759 -0
- package/src/plugin/lcm-doctor-apply.ts +546 -0
- package/src/plugin/lcm-doctor-shared.ts +210 -0
- package/src/store/conversation-store.ts +60 -21
- package/src/store/parse-utc-timestamp.ts +25 -0
- package/src/store/summary-store.ts +460 -11
- package/src/summarize.ts +553 -224
- package/src/tools/lcm-expand-query-tool.ts +195 -59
- package/src/tools/lcm-expansion-recursion-guard.ts +87 -0
- package/src/types.ts +1 -0
package/src/plugin/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { createLcmDescribeTool } from "../tools/lcm-describe-tool.js";
|
|
|
15
15
|
import { createLcmExpandQueryTool } from "../tools/lcm-expand-query-tool.js";
|
|
16
16
|
import { createLcmExpandTool } from "../tools/lcm-expand-tool.js";
|
|
17
17
|
import { createLcmGrepTool } from "../tools/lcm-grep-tool.js";
|
|
18
|
+
import { createLcmCommand } from "./lcm-command.js";
|
|
18
19
|
import type { LcmDependencies } from "../types.js";
|
|
19
20
|
|
|
20
21
|
/** Parse `agent:<agentId>:<suffix...>` session keys. */
|
|
@@ -112,6 +113,33 @@ type CompletionBridgeErrorInfo = {
|
|
|
112
113
|
message?: string;
|
|
113
114
|
};
|
|
114
115
|
|
|
116
|
+
const LOSSLESS_RECALL_POLICY_PROMPT = [
|
|
117
|
+
"## Lossless Recall Policy",
|
|
118
|
+
"",
|
|
119
|
+
"The lossless-claw plugin is active.",
|
|
120
|
+
"",
|
|
121
|
+
"For compacted conversation history, these instructions supersede generic memory-recall guidance. Prefer lossless-claw recall tools first when answering questions about prior conversation content, decisions made in the conversation, or details that may have been compacted.",
|
|
122
|
+
"",
|
|
123
|
+
"**Conflict handling:** If newer evidence conflicts with an older summary or recollection, prefer the newer evidence. Do not trust a stale summary over fresher contradictory information.",
|
|
124
|
+
"",
|
|
125
|
+
"**Contradictions/uncertainty:** If facts seem contradictory or uncertain, verify with lossless-claw recall tools before answering instead of trusting the summary at face value.",
|
|
126
|
+
"",
|
|
127
|
+
"**Tool escalation:**",
|
|
128
|
+
"Recall order for compacted conversation history:",
|
|
129
|
+
"1. `lcm_grep` — search by regex or full-text across messages and summaries",
|
|
130
|
+
"2. `lcm_describe` — inspect a specific summary (cheap, no sub-agent)",
|
|
131
|
+
"3. `lcm_expand_query` — deep recall: spawns bounded sub-agent, expands DAG, returns answer with cited summary IDs (~120s, don't ration it)",
|
|
132
|
+
"",
|
|
133
|
+
"**`lcm_expand_query` usage** — two patterns (always requires `prompt`):",
|
|
134
|
+
"- With IDs: `lcm_expand_query(summaryIds: [\"sum_xxx\"], prompt: \"What config changes were discussed?\")`",
|
|
135
|
+
"- With search: `lcm_expand_query(query: \"database migration\", prompt: \"What strategy was decided?\")`",
|
|
136
|
+
"- Optional: `maxTokens` (default 2000), `conversationId`, `allConversations: true`",
|
|
137
|
+
"",
|
|
138
|
+
"These precedence rules apply only to compacted conversation history. Lossless-claw does not supersede memory tools globally.",
|
|
139
|
+
"",
|
|
140
|
+
"If a summary conflicts with newer evidence, prefer the newer evidence. Do not guess exact commands, SHAs, paths, timestamps, config values, or causal claims from compacted summaries when expansion is needed.",
|
|
141
|
+
].join("\n");
|
|
142
|
+
|
|
115
143
|
/** Capture plugin env values once during initialization. */
|
|
116
144
|
function snapshotPluginEnv(env: NodeJS.ProcessEnv = process.env): PluginEnvSnapshot {
|
|
117
145
|
return {
|
|
@@ -228,6 +256,30 @@ function readDefaultModelFromConfig(config: unknown): string {
|
|
|
228
256
|
return typeof primary === "string" ? primary.trim() : "";
|
|
229
257
|
}
|
|
230
258
|
|
|
259
|
+
/** Read OpenClaw's configured compaction model from the validated runtime config. */
|
|
260
|
+
function readCompactionModelFromConfig(config: unknown): string {
|
|
261
|
+
if (!config || typeof config !== "object") {
|
|
262
|
+
return "";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const compaction = (config as {
|
|
266
|
+
agents?: {
|
|
267
|
+
defaults?: {
|
|
268
|
+
compaction?: {
|
|
269
|
+
model?: unknown;
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
}).agents?.defaults?.compaction;
|
|
274
|
+
const model = compaction?.model;
|
|
275
|
+
if (typeof model === "string") {
|
|
276
|
+
return model.trim();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const primary = (model as { primary?: unknown } | undefined)?.primary;
|
|
280
|
+
return typeof primary === "string" ? primary.trim() : "";
|
|
281
|
+
}
|
|
282
|
+
|
|
231
283
|
/** Format a provider/model pair for logs. */
|
|
232
284
|
function formatProviderModel(params: { provider: string; model: string }): string {
|
|
233
285
|
return `${params.provider}/${params.model}`;
|
|
@@ -236,11 +288,28 @@ function formatProviderModel(params: { provider: string; model: string }): strin
|
|
|
236
288
|
/** Build a startup log showing which compaction model LCM will use. */
|
|
237
289
|
function buildCompactionModelLog(params: {
|
|
238
290
|
config: LcmConfig;
|
|
239
|
-
|
|
291
|
+
openClawConfig: unknown;
|
|
240
292
|
defaultProvider: string;
|
|
241
293
|
}): string {
|
|
242
|
-
const
|
|
243
|
-
const
|
|
294
|
+
const envSummaryModel = process.env.LCM_SUMMARY_MODEL?.trim() ?? "";
|
|
295
|
+
const envSummaryProvider = process.env.LCM_SUMMARY_PROVIDER?.trim() ?? "";
|
|
296
|
+
const pluginSummaryModel = params.config.summaryModel.trim();
|
|
297
|
+
const pluginSummaryProvider = params.config.summaryProvider.trim();
|
|
298
|
+
const compactionModelRef = readCompactionModelFromConfig(params.openClawConfig);
|
|
299
|
+
const defaultModelRef = readDefaultModelFromConfig(params.openClawConfig);
|
|
300
|
+
const selected =
|
|
301
|
+
envSummaryModel
|
|
302
|
+
? { raw: envSummaryModel, source: "override" as const }
|
|
303
|
+
: pluginSummaryModel
|
|
304
|
+
? { raw: pluginSummaryModel, source: "override" as const }
|
|
305
|
+
: compactionModelRef
|
|
306
|
+
? { raw: compactionModelRef, source: "override" as const }
|
|
307
|
+
: defaultModelRef
|
|
308
|
+
? { raw: defaultModelRef, source: "default" as const }
|
|
309
|
+
: undefined;
|
|
310
|
+
const usingOverride =
|
|
311
|
+
selected?.source === "override" || Boolean(envSummaryProvider || pluginSummaryProvider);
|
|
312
|
+
const raw = selected?.raw.trim() ?? "";
|
|
244
313
|
if (!raw) {
|
|
245
314
|
return "[lcm] Compaction summarization model: (unconfigured)";
|
|
246
315
|
}
|
|
@@ -256,7 +325,12 @@ function buildCompactionModelLog(params: {
|
|
|
256
325
|
}
|
|
257
326
|
}
|
|
258
327
|
|
|
259
|
-
const provider = (
|
|
328
|
+
const provider = (
|
|
329
|
+
envSummaryProvider ||
|
|
330
|
+
pluginSummaryProvider ||
|
|
331
|
+
params.defaultProvider ||
|
|
332
|
+
"openai"
|
|
333
|
+
).trim();
|
|
260
334
|
return `[lcm] Compaction summarization model: ${formatProviderModel({
|
|
261
335
|
provider,
|
|
262
336
|
model: raw,
|
|
@@ -1037,6 +1111,64 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1037
1111
|
api.logger.warn(buildLegacyAuthFallbackWarning());
|
|
1038
1112
|
}
|
|
1039
1113
|
|
|
1114
|
+
/** Resolve the best config object to hand to runtime.modelAuth for this lookup. */
|
|
1115
|
+
const resolveModelAuthConfig = (runtimeConfig: unknown): OpenClawPluginApi["config"] => {
|
|
1116
|
+
if (runtimeConfig && typeof runtimeConfig === "object") {
|
|
1117
|
+
return runtimeConfig as OpenClawPluginApi["config"];
|
|
1118
|
+
}
|
|
1119
|
+
return api.config;
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
/** Resolve an API key without throwing so summarizer auth fallback can retry safely. */
|
|
1123
|
+
const lookupApiKey = async (
|
|
1124
|
+
provider: string,
|
|
1125
|
+
model: string,
|
|
1126
|
+
options?: {
|
|
1127
|
+
profileId?: string;
|
|
1128
|
+
preferredProfile?: string;
|
|
1129
|
+
agentDir?: string;
|
|
1130
|
+
runtimeConfig?: unknown;
|
|
1131
|
+
skipModelAuth?: boolean;
|
|
1132
|
+
},
|
|
1133
|
+
): Promise<string | undefined> => {
|
|
1134
|
+
const modelAuthConfig = resolveModelAuthConfig(options?.runtimeConfig);
|
|
1135
|
+
|
|
1136
|
+
if (modelAuth && options?.skipModelAuth !== true) {
|
|
1137
|
+
try {
|
|
1138
|
+
const modelAuthKey = resolveApiKeyFromAuthResult(
|
|
1139
|
+
await modelAuth.getApiKeyForModel({
|
|
1140
|
+
model: buildModelAuthLookupModel({ provider, model }),
|
|
1141
|
+
cfg: modelAuthConfig,
|
|
1142
|
+
...(options?.profileId ? { profileId: options.profileId } : {}),
|
|
1143
|
+
...(options?.preferredProfile ? { preferredProfile: options.preferredProfile } : {}),
|
|
1144
|
+
}),
|
|
1145
|
+
);
|
|
1146
|
+
if (modelAuthKey) {
|
|
1147
|
+
return modelAuthKey;
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
// Fall through to env/auth-profile lookup for older or scope-limited runtimes.
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const envKey = resolveApiKey(provider, readEnv);
|
|
1155
|
+
if (envKey) {
|
|
1156
|
+
return envKey;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const piAiModuleId = "@mariozechner/pi-ai";
|
|
1160
|
+
const mod = (await import(piAiModuleId)) as PiAiModule;
|
|
1161
|
+
return resolveApiKeyFromAuthProfiles({
|
|
1162
|
+
provider,
|
|
1163
|
+
authProfileId: options?.profileId,
|
|
1164
|
+
agentDir: options?.agentDir ?? api.resolvePath("."),
|
|
1165
|
+
runtimeConfig: options?.runtimeConfig,
|
|
1166
|
+
appConfig: api.config,
|
|
1167
|
+
piAiModule: mod,
|
|
1168
|
+
envSnapshot,
|
|
1169
|
+
});
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1040
1172
|
return {
|
|
1041
1173
|
config,
|
|
1042
1174
|
complete: async ({
|
|
@@ -1349,76 +1481,10 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1349
1481
|
return { provider, model: raw };
|
|
1350
1482
|
},
|
|
1351
1483
|
getApiKey: async (provider, model, options) => {
|
|
1352
|
-
|
|
1353
|
-
try {
|
|
1354
|
-
const modelAuthKey = resolveApiKeyFromAuthResult(
|
|
1355
|
-
await modelAuth.getApiKeyForModel({
|
|
1356
|
-
model: buildModelAuthLookupModel({ provider, model }),
|
|
1357
|
-
cfg: api.config,
|
|
1358
|
-
...(options?.profileId ? { profileId: options.profileId } : {}),
|
|
1359
|
-
...(options?.preferredProfile ? { preferredProfile: options.preferredProfile } : {}),
|
|
1360
|
-
}),
|
|
1361
|
-
);
|
|
1362
|
-
if (modelAuthKey) {
|
|
1363
|
-
return modelAuthKey;
|
|
1364
|
-
}
|
|
1365
|
-
} catch {
|
|
1366
|
-
// Fall through to auth-profile lookup for older OpenClaw runtimes.
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
const envKey = resolveApiKey(provider, readEnv);
|
|
1371
|
-
if (envKey) {
|
|
1372
|
-
return envKey;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
const piAiModuleId = "@mariozechner/pi-ai";
|
|
1376
|
-
const mod = (await import(piAiModuleId)) as PiAiModule;
|
|
1377
|
-
return resolveApiKeyFromAuthProfiles({
|
|
1378
|
-
provider,
|
|
1379
|
-
authProfileId: options?.profileId,
|
|
1380
|
-
agentDir: api.resolvePath("."),
|
|
1381
|
-
runtimeConfig: api.config,
|
|
1382
|
-
piAiModule: mod,
|
|
1383
|
-
envSnapshot,
|
|
1384
|
-
});
|
|
1484
|
+
return lookupApiKey(provider, model, options);
|
|
1385
1485
|
},
|
|
1386
1486
|
requireApiKey: async (provider, model, options) => {
|
|
1387
|
-
const key = await (
|
|
1388
|
-
if (modelAuth) {
|
|
1389
|
-
try {
|
|
1390
|
-
const modelAuthKey = resolveApiKeyFromAuthResult(
|
|
1391
|
-
await modelAuth.getApiKeyForModel({
|
|
1392
|
-
model: buildModelAuthLookupModel({ provider, model }),
|
|
1393
|
-
cfg: api.config,
|
|
1394
|
-
...(options?.profileId ? { profileId: options.profileId } : {}),
|
|
1395
|
-
...(options?.preferredProfile ? { preferredProfile: options.preferredProfile } : {}),
|
|
1396
|
-
}),
|
|
1397
|
-
);
|
|
1398
|
-
if (modelAuthKey) {
|
|
1399
|
-
return modelAuthKey;
|
|
1400
|
-
}
|
|
1401
|
-
} catch {
|
|
1402
|
-
// Fall through to auth-profile lookup for older OpenClaw runtimes.
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
const envKey = resolveApiKey(provider, readEnv);
|
|
1407
|
-
if (envKey) {
|
|
1408
|
-
return envKey;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
const piAiModuleId = "@mariozechner/pi-ai";
|
|
1412
|
-
const mod = (await import(piAiModuleId)) as PiAiModule;
|
|
1413
|
-
return resolveApiKeyFromAuthProfiles({
|
|
1414
|
-
provider,
|
|
1415
|
-
authProfileId: options?.profileId,
|
|
1416
|
-
agentDir: api.resolvePath("."),
|
|
1417
|
-
runtimeConfig: api.config,
|
|
1418
|
-
piAiModule: mod,
|
|
1419
|
-
envSnapshot,
|
|
1420
|
-
});
|
|
1421
|
-
})();
|
|
1487
|
+
const key = await lookupApiKey(provider, model, options);
|
|
1422
1488
|
if (!key) {
|
|
1423
1489
|
throw new Error(`Missing API key for provider '${provider}' (model '${model}').`);
|
|
1424
1490
|
}
|
|
@@ -1485,6 +1551,16 @@ const lcmPlugin = {
|
|
|
1485
1551
|
const database = createLcmDatabaseConnection(deps.config.databasePath);
|
|
1486
1552
|
const lcm = new LcmContextEngine(deps, database);
|
|
1487
1553
|
|
|
1554
|
+
api.on("before_reset", async (event, ctx) => {
|
|
1555
|
+
await lcm.handleBeforeReset({
|
|
1556
|
+
reason: event.reason,
|
|
1557
|
+
sessionId: ctx.sessionId,
|
|
1558
|
+
sessionKey: ctx.sessionKey,
|
|
1559
|
+
});
|
|
1560
|
+
});
|
|
1561
|
+
api.on("before_prompt_build", () => ({
|
|
1562
|
+
prependSystemContext: LOSSLESS_RECALL_POLICY_PROMPT,
|
|
1563
|
+
}));
|
|
1488
1564
|
api.registerContextEngine("lossless-claw", () => lcm);
|
|
1489
1565
|
api.registerContextEngine("default", () => lcm);
|
|
1490
1566
|
api.registerTool((ctx) =>
|
|
@@ -1516,6 +1592,13 @@ const lcmPlugin = {
|
|
|
1516
1592
|
requesterSessionKey: ctx.sessionKey,
|
|
1517
1593
|
}),
|
|
1518
1594
|
);
|
|
1595
|
+
api.registerCommand(
|
|
1596
|
+
createLcmCommand({
|
|
1597
|
+
db: database,
|
|
1598
|
+
config: deps.config,
|
|
1599
|
+
deps,
|
|
1600
|
+
}),
|
|
1601
|
+
);
|
|
1519
1602
|
|
|
1520
1603
|
logStartupBannerOnce({
|
|
1521
1604
|
key: "plugin-loaded",
|
|
@@ -1527,7 +1610,7 @@ const lcmPlugin = {
|
|
|
1527
1610
|
log: (message) => api.logger.info(message),
|
|
1528
1611
|
message: buildCompactionModelLog({
|
|
1529
1612
|
config: deps.config,
|
|
1530
|
-
|
|
1613
|
+
openClawConfig: api.config,
|
|
1531
1614
|
defaultProvider: process.env.OPENCLAW_PROVIDER?.trim() ?? "",
|
|
1532
1615
|
}),
|
|
1533
1616
|
});
|