@martian-engineering/lossless-claw 0.5.2 → 0.5.3

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.
@@ -228,6 +228,30 @@ function readDefaultModelFromConfig(config: unknown): string {
228
228
  return typeof primary === "string" ? primary.trim() : "";
229
229
  }
230
230
 
231
+ /** Read OpenClaw's configured compaction model from the validated runtime config. */
232
+ function readCompactionModelFromConfig(config: unknown): string {
233
+ if (!config || typeof config !== "object") {
234
+ return "";
235
+ }
236
+
237
+ const compaction = (config as {
238
+ agents?: {
239
+ defaults?: {
240
+ compaction?: {
241
+ model?: unknown;
242
+ };
243
+ };
244
+ };
245
+ }).agents?.defaults?.compaction;
246
+ const model = compaction?.model;
247
+ if (typeof model === "string") {
248
+ return model.trim();
249
+ }
250
+
251
+ const primary = (model as { primary?: unknown } | undefined)?.primary;
252
+ return typeof primary === "string" ? primary.trim() : "";
253
+ }
254
+
231
255
  /** Format a provider/model pair for logs. */
232
256
  function formatProviderModel(params: { provider: string; model: string }): string {
233
257
  return `${params.provider}/${params.model}`;
@@ -236,11 +260,28 @@ function formatProviderModel(params: { provider: string; model: string }): strin
236
260
  /** Build a startup log showing which compaction model LCM will use. */
237
261
  function buildCompactionModelLog(params: {
238
262
  config: LcmConfig;
239
- defaultModelRef: string;
263
+ openClawConfig: unknown;
240
264
  defaultProvider: string;
241
265
  }): string {
242
- const usingOverride = Boolean(params.config.summaryModel || params.config.summaryProvider);
243
- const raw = (params.config.summaryModel || params.defaultModelRef).trim();
266
+ const envSummaryModel = process.env.LCM_SUMMARY_MODEL?.trim() ?? "";
267
+ const envSummaryProvider = process.env.LCM_SUMMARY_PROVIDER?.trim() ?? "";
268
+ const pluginSummaryModel = params.config.summaryModel.trim();
269
+ const pluginSummaryProvider = params.config.summaryProvider.trim();
270
+ const compactionModelRef = readCompactionModelFromConfig(params.openClawConfig);
271
+ const defaultModelRef = readDefaultModelFromConfig(params.openClawConfig);
272
+ const selected =
273
+ envSummaryModel
274
+ ? { raw: envSummaryModel, source: "override" as const }
275
+ : pluginSummaryModel
276
+ ? { raw: pluginSummaryModel, source: "override" as const }
277
+ : compactionModelRef
278
+ ? { raw: compactionModelRef, source: "override" as const }
279
+ : defaultModelRef
280
+ ? { raw: defaultModelRef, source: "default" as const }
281
+ : undefined;
282
+ const usingOverride =
283
+ selected?.source === "override" || Boolean(envSummaryProvider || pluginSummaryProvider);
284
+ const raw = selected?.raw.trim() ?? "";
244
285
  if (!raw) {
245
286
  return "[lcm] Compaction summarization model: (unconfigured)";
246
287
  }
@@ -256,7 +297,12 @@ function buildCompactionModelLog(params: {
256
297
  }
257
298
  }
258
299
 
259
- const provider = (params.config.summaryProvider || params.defaultProvider || "openai").trim();
300
+ const provider = (
301
+ envSummaryProvider ||
302
+ pluginSummaryProvider ||
303
+ params.defaultProvider ||
304
+ "openai"
305
+ ).trim();
260
306
  return `[lcm] Compaction summarization model: ${formatProviderModel({
261
307
  provider,
262
308
  model: raw,
@@ -1037,6 +1083,64 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
1037
1083
  api.logger.warn(buildLegacyAuthFallbackWarning());
1038
1084
  }
1039
1085
 
1086
+ /** Resolve the best config object to hand to runtime.modelAuth for this lookup. */
1087
+ const resolveModelAuthConfig = (runtimeConfig: unknown): OpenClawPluginApi["config"] => {
1088
+ if (runtimeConfig && typeof runtimeConfig === "object") {
1089
+ return runtimeConfig as OpenClawPluginApi["config"];
1090
+ }
1091
+ return api.config;
1092
+ };
1093
+
1094
+ /** Resolve an API key without throwing so summarizer auth fallback can retry safely. */
1095
+ const lookupApiKey = async (
1096
+ provider: string,
1097
+ model: string,
1098
+ options?: {
1099
+ profileId?: string;
1100
+ preferredProfile?: string;
1101
+ agentDir?: string;
1102
+ runtimeConfig?: unknown;
1103
+ skipModelAuth?: boolean;
1104
+ },
1105
+ ): Promise<string | undefined> => {
1106
+ const modelAuthConfig = resolveModelAuthConfig(options?.runtimeConfig);
1107
+
1108
+ if (modelAuth && options?.skipModelAuth !== true) {
1109
+ try {
1110
+ const modelAuthKey = resolveApiKeyFromAuthResult(
1111
+ await modelAuth.getApiKeyForModel({
1112
+ model: buildModelAuthLookupModel({ provider, model }),
1113
+ cfg: modelAuthConfig,
1114
+ ...(options?.profileId ? { profileId: options.profileId } : {}),
1115
+ ...(options?.preferredProfile ? { preferredProfile: options.preferredProfile } : {}),
1116
+ }),
1117
+ );
1118
+ if (modelAuthKey) {
1119
+ return modelAuthKey;
1120
+ }
1121
+ } catch {
1122
+ // Fall through to env/auth-profile lookup for older or scope-limited runtimes.
1123
+ }
1124
+ }
1125
+
1126
+ const envKey = resolveApiKey(provider, readEnv);
1127
+ if (envKey) {
1128
+ return envKey;
1129
+ }
1130
+
1131
+ const piAiModuleId = "@mariozechner/pi-ai";
1132
+ const mod = (await import(piAiModuleId)) as PiAiModule;
1133
+ return resolveApiKeyFromAuthProfiles({
1134
+ provider,
1135
+ authProfileId: options?.profileId,
1136
+ agentDir: options?.agentDir ?? api.resolvePath("."),
1137
+ runtimeConfig: options?.runtimeConfig,
1138
+ appConfig: api.config,
1139
+ piAiModule: mod,
1140
+ envSnapshot,
1141
+ });
1142
+ };
1143
+
1040
1144
  return {
1041
1145
  config,
1042
1146
  complete: async ({
@@ -1349,76 +1453,10 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
1349
1453
  return { provider, model: raw };
1350
1454
  },
1351
1455
  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
- });
1456
+ return lookupApiKey(provider, model, options);
1385
1457
  },
1386
1458
  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
- })();
1459
+ const key = await lookupApiKey(provider, model, options);
1422
1460
  if (!key) {
1423
1461
  throw new Error(`Missing API key for provider '${provider}' (model '${model}').`);
1424
1462
  }
@@ -1527,7 +1565,7 @@ const lcmPlugin = {
1527
1565
  log: (message) => api.logger.info(message),
1528
1566
  message: buildCompactionModelLog({
1529
1567
  config: deps.config,
1530
- defaultModelRef: readDefaultModelFromConfig(api.config),
1568
+ openClawConfig: api.config,
1531
1569
  defaultProvider: process.env.OPENCLAW_PROVIDER?.trim() ?? "",
1532
1570
  }),
1533
1571
  });
@@ -45,6 +45,11 @@ export type SummarySubtreeNodeRecord = SummaryRecord & {
45
45
  childCount: number;
46
46
  };
47
47
 
48
+ export type MessageLeafSummaryLinkRecord = {
49
+ messageId: number;
50
+ summaryId: string;
51
+ };
52
+
48
53
  export type ContextItemRecord = {
49
54
  conversationId: number;
50
55
  ordinal: number;
@@ -172,6 +177,15 @@ interface MessageIdRow {
172
177
  message_id: number;
173
178
  }
174
179
 
180
+ interface MaxDepthRow {
181
+ max_depth: number | null;
182
+ }
183
+
184
+ interface MessageLeafSummaryLinkRow {
185
+ message_id: number;
186
+ summary_id: string;
187
+ }
188
+
175
189
  interface LargeFileRow {
176
190
  file_id: string;
177
191
  conversation_id: number;
@@ -460,6 +474,72 @@ export class SummaryStore {
460
474
  return rows.map((r) => r.message_id);
461
475
  }
462
476
 
477
+ /**
478
+ * Return the deepest persisted summary depth for a conversation.
479
+ */
480
+ async getConversationMaxSummaryDepth(conversationId: number): Promise<number | null> {
481
+ const row = this.db
482
+ .prepare(
483
+ `SELECT MAX(depth) AS max_depth
484
+ FROM summaries
485
+ WHERE conversation_id = ?`,
486
+ )
487
+ .get(conversationId) as unknown as MaxDepthRow | undefined;
488
+ return typeof row?.max_depth === "number" ? row.max_depth : null;
489
+ }
490
+
491
+ /**
492
+ * Resolve raw message hits back to their linked leaf summaries.
493
+ */
494
+ async getLeafSummaryLinksForMessageIds(
495
+ conversationId: number,
496
+ messageIds: number[],
497
+ ): Promise<MessageLeafSummaryLinkRecord[]> {
498
+ const normalizedMessageIds = Array.from(
499
+ new Set(
500
+ messageIds.filter(
501
+ (messageId): messageId is number => Number.isInteger(messageId) && messageId > 0,
502
+ ),
503
+ ),
504
+ );
505
+ if (normalizedMessageIds.length === 0) {
506
+ return [];
507
+ }
508
+
509
+ const placeholders = normalizedMessageIds.map(() => "?").join(", ");
510
+ const rows = this.db
511
+ .prepare(
512
+ `SELECT sm.message_id, sm.summary_id
513
+ FROM summary_messages sm
514
+ JOIN summaries s ON s.summary_id = sm.summary_id
515
+ WHERE s.conversation_id = ?
516
+ AND s.kind = 'leaf'
517
+ AND sm.message_id IN (${placeholders})
518
+ ORDER BY sm.ordinal ASC, s.created_at ASC`,
519
+ )
520
+ .all(conversationId, ...normalizedMessageIds) as unknown as MessageLeafSummaryLinkRow[];
521
+
522
+ const summaryIdsByMessageId = new Map<number, string[]>();
523
+ for (const row of rows) {
524
+ const existing = summaryIdsByMessageId.get(row.message_id) ?? [];
525
+ if (!existing.includes(row.summary_id)) {
526
+ existing.push(row.summary_id);
527
+ summaryIdsByMessageId.set(row.message_id, existing);
528
+ }
529
+ }
530
+
531
+ const orderedLinks: MessageLeafSummaryLinkRecord[] = [];
532
+ for (const messageId of normalizedMessageIds) {
533
+ for (const summaryId of summaryIdsByMessageId.get(messageId) ?? []) {
534
+ orderedLinks.push({
535
+ messageId,
536
+ summaryId,
537
+ });
538
+ }
539
+ }
540
+ return orderedLinks;
541
+ }
542
+
463
543
  async getSummaryChildren(parentSummaryId: string): Promise<SummaryRecord[]> {
464
544
  const rows = this.db
465
545
  .prepare(