@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.
@@ -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
- defaultModelRef: string;
291
+ openClawConfig: unknown;
240
292
  defaultProvider: string;
241
293
  }): string {
242
- const usingOverride = Boolean(params.config.summaryModel || params.config.summaryProvider);
243
- const raw = (params.config.summaryModel || params.defaultModelRef).trim();
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 = (params.config.summaryProvider || params.defaultProvider || "openai").trim();
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
- if (modelAuth) {
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 (async () => {
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
- defaultModelRef: readDefaultModelFromConfig(api.config),
1613
+ openClawConfig: api.config,
1531
1614
  defaultProvider: process.env.OPENCLAW_PROVIDER?.trim() ?? "",
1532
1615
  }),
1533
1616
  });