@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.
- package/README.md +18 -10
- package/docs/configuration.md +21 -0
- package/openclaw.plugin.json +39 -0
- package/package.json +1 -1
- package/src/assembler.ts +194 -3
- package/src/compaction.ts +203 -18
- package/src/db/config.ts +24 -3
- package/src/engine.ts +25 -6
- package/src/plugin/index.ts +111 -73
- package/src/store/summary-store.ts +80 -0
- package/src/summarize.ts +451 -209
- package/src/tools/lcm-expand-query-tool.ts +137 -34
- package/src/types.ts +1 -0
package/src/plugin/index.ts
CHANGED
|
@@ -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
|
-
|
|
263
|
+
openClawConfig: unknown;
|
|
240
264
|
defaultProvider: string;
|
|
241
265
|
}): string {
|
|
242
|
-
const
|
|
243
|
-
const
|
|
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 = (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|