@martian-engineering/lossless-claw 0.6.3 → 0.8.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 +26 -6
- package/docs/agent-tools.md +16 -5
- package/docs/configuration.md +223 -214
- package/openclaw.plugin.json +123 -0
- package/package.json +1 -1
- package/skills/lossless-claw/SKILL.md +3 -2
- package/skills/lossless-claw/references/architecture.md +12 -0
- package/skills/lossless-claw/references/config.md +135 -3
- package/skills/lossless-claw/references/diagnostics.md +13 -0
- package/src/assembler.ts +17 -5
- package/src/compaction.ts +161 -53
- package/src/db/config.ts +102 -4
- package/src/db/connection.ts +35 -7
- package/src/db/features.ts +24 -5
- package/src/db/migration.ts +257 -78
- package/src/engine.ts +1007 -110
- package/src/estimate-tokens.ts +80 -0
- package/src/lcm-log.ts +37 -0
- package/src/plugin/index.ts +493 -101
- package/src/plugin/lcm-command.ts +288 -7
- package/src/plugin/lcm-doctor-apply.ts +1 -3
- package/src/plugin/lcm-doctor-cleaners.ts +655 -0
- package/src/plugin/shared-init.ts +59 -0
- package/src/prune.ts +391 -0
- package/src/retrieval.ts +8 -9
- package/src/startup-banner-log.ts +1 -0
- package/src/store/compaction-telemetry-store.ts +156 -0
- package/src/store/conversation-store.ts +6 -1
- package/src/store/fts5-sanitize.ts +25 -4
- package/src/store/full-text-sort.ts +21 -0
- package/src/store/index.ts +8 -0
- package/src/store/summary-store.ts +21 -14
- package/src/summarize.ts +55 -34
- package/src/tools/lcm-describe-tool.ts +9 -4
- package/src/tools/lcm-expand-query-tool.ts +609 -200
- package/src/tools/lcm-expand-tool.ts +9 -4
- package/src/tools/lcm-grep-tool.ts +22 -8
- package/src/types.ts +1 -0
package/src/plugin/index.ts
CHANGED
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
9
10
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
10
11
|
import { resolveLcmConfig } from "../db/config.js";
|
|
11
|
-
import { createLcmDatabaseConnection } from "../db/connection.js";
|
|
12
|
+
import { closeLcmConnection, createLcmDatabaseConnection, normalizePath } from "../db/connection.js";
|
|
12
13
|
import { LcmContextEngine } from "../engine.js";
|
|
14
|
+
import { createLcmLogger, describeLogError } from "../lcm-log.js";
|
|
13
15
|
import { logStartupBannerOnce } from "../startup-banner-log.js";
|
|
16
|
+
import { getSharedInit, setSharedInit, removeSharedInit } from "./shared-init.js";
|
|
17
|
+
import type { SharedLcmInit } from "./shared-init.js";
|
|
14
18
|
import { createLcmDescribeTool } from "../tools/lcm-describe-tool.js";
|
|
15
19
|
import { createLcmExpandQueryTool } from "../tools/lcm-expand-query-tool.js";
|
|
16
20
|
import { createLcmExpandTool } from "../tools/lcm-expand-tool.js";
|
|
@@ -64,6 +68,29 @@ type CompleteSimpleOptions = {
|
|
|
64
68
|
|
|
65
69
|
type RuntimeModelAuthResult = {
|
|
66
70
|
apiKey?: string;
|
|
71
|
+
baseUrl?: string;
|
|
72
|
+
request?: RuntimeModelRequestTransportOverrides;
|
|
73
|
+
expiresAt?: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type RuntimeModelRequestAuthOverride =
|
|
77
|
+
| {
|
|
78
|
+
mode: "provider-default";
|
|
79
|
+
}
|
|
80
|
+
| {
|
|
81
|
+
mode: "authorization-bearer";
|
|
82
|
+
token: string;
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
mode: "header";
|
|
86
|
+
headerName: string;
|
|
87
|
+
value: string;
|
|
88
|
+
prefix?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type RuntimeModelRequestTransportOverrides = {
|
|
92
|
+
headers?: Record<string, string>;
|
|
93
|
+
auth?: RuntimeModelRequestAuthOverride;
|
|
67
94
|
};
|
|
68
95
|
|
|
69
96
|
type SessionEndLifecycleEvent = {
|
|
@@ -104,11 +131,19 @@ type RuntimeModelAuth = {
|
|
|
104
131
|
profileId?: string;
|
|
105
132
|
preferredProfile?: string;
|
|
106
133
|
}) => Promise<RuntimeModelAuthResult | undefined>;
|
|
134
|
+
getRuntimeAuthForModel?: (params: {
|
|
135
|
+
model: RuntimeModelAuthModel;
|
|
136
|
+
cfg?: OpenClawPluginApi["config"];
|
|
137
|
+
profileId?: string;
|
|
138
|
+
preferredProfile?: string;
|
|
139
|
+
workspaceDir?: string;
|
|
140
|
+
}) => Promise<RuntimeModelAuthResult | undefined>;
|
|
107
141
|
};
|
|
108
142
|
|
|
109
143
|
const MODEL_AUTH_PR_URL = "https://github.com/openclaw/openclaw/pull/41090";
|
|
110
144
|
const MODEL_AUTH_MERGE_COMMIT = "4790e40";
|
|
111
145
|
const MODEL_AUTH_REQUIRED_RELEASE = "the first OpenClaw release after 2026.3.8";
|
|
146
|
+
const PROVIDER_API_RESOLUTION_ERROR_PREFIX = "[lcm] unable to resolve API family for provider ";
|
|
112
147
|
const AUTH_ERROR_TEXT_PATTERN =
|
|
113
148
|
/\b401\b|unauthorized|unauthorised|invalid[_ -]?token|invalid[_ -]?api[_ -]?key|authentication failed|authorization failed|missing scope|insufficient scope|model\.request\b/i;
|
|
114
149
|
const AUTH_ERROR_STATUS_KEYS = ["status", "statusCode", "status_code"] as const;
|
|
@@ -136,12 +171,33 @@ const LOSSLESS_RECALL_POLICY_PROMPT = [
|
|
|
136
171
|
"Recall order for compacted conversation history:",
|
|
137
172
|
"1. `lcm_grep` — search by regex or full-text across messages and summaries",
|
|
138
173
|
"2. `lcm_describe` — inspect a specific summary (cheap, no sub-agent)",
|
|
139
|
-
"3. `lcm_expand_query` — deep recall: spawns bounded sub-agent, expands DAG, returns answer
|
|
174
|
+
"3. `lcm_expand_query` — deep recall: spawns bounded sub-agent, expands DAG, and returns answer plus cited summary IDs in tool output for follow-up (~120s, don't ration it)",
|
|
175
|
+
"",
|
|
176
|
+
"**`lcm_grep` routing guidance:**",
|
|
177
|
+
'- Prefer `mode: "full_text"` for keyword or topical recall; keep `mode: "regex"` for literal patterns.',
|
|
178
|
+
'- Full-text queries use FTS5 semantics, and FTS5 defaults to AND matching, so extra terms make matching stricter rather than broader.',
|
|
179
|
+
'- Prefer 1-3 distinctive full-text terms or one quoted phrase. Do not pad queries with synonyms or extra keywords.',
|
|
180
|
+
'- Wrap exact multi-word phrases in quotes, for example `"error handling"`.',
|
|
181
|
+
'- Keep the default `sort: "recency"` for "what just happened?" lookups.',
|
|
182
|
+
'- Use `sort: "relevance"` when hunting for the best older match on a topic.',
|
|
183
|
+
'- Use `sort: "hybrid"` when relevance matters but newer context should still get a boost.',
|
|
140
184
|
"",
|
|
141
185
|
"**`lcm_expand_query` usage** — two patterns (always requires `prompt`):",
|
|
142
186
|
"- With IDs: `lcm_expand_query(summaryIds: [\"sum_xxx\"], prompt: \"What config changes were discussed?\")`",
|
|
143
187
|
"- With search: `lcm_expand_query(query: \"database migration\", prompt: \"What strategy was decided?\")`",
|
|
188
|
+
"- `query` uses the same FTS5 full-text search path as `lcm_grep`, so the same query-construction rules apply.",
|
|
189
|
+
"- `query` is for matching candidate summaries; `prompt` is the natural-language question or task to answer after expansion.",
|
|
190
|
+
"- FTS5 defaults to AND matching, so more query terms narrow results instead of broadening them.",
|
|
191
|
+
"- For `query`, use 1-3 distinctive terms or a quoted phrase. Do not stuff synonyms or extra keywords into it.",
|
|
192
|
+
"**Scope selection rule:**",
|
|
193
|
+
"- Start with the current conversation scope.",
|
|
194
|
+
"- If the in-context summaries already look relevant to the user's question, prefer `lcm_grep` or `lcm_expand_query` without `allConversations`.",
|
|
195
|
+
"- Use `allConversations: true` only when the current summaries do not appear sufficient, the question seems outside the current conversation, or the user is explicitly asking about work across sessions.",
|
|
196
|
+
"- For global discovery, prefer `lcm_grep(..., allConversations: true)` first.",
|
|
197
|
+
"- If global matches are found and the user needs one synthesized answer, use `lcm_expand_query(..., allConversations: true)`; this is bounded synthesis, not exhaustive expansion.",
|
|
198
|
+
"- If you already know the exact target conversation, prefer explicit `conversationId` instead of `allConversations`.",
|
|
144
199
|
"- Optional: `maxTokens` (default 2000), `conversationId`, `allConversations: true`",
|
|
200
|
+
"- Keep raw summary IDs out of normal user-facing prose unless the user explicitly asks for sources or IDs.",
|
|
145
201
|
"",
|
|
146
202
|
"These precedence rules apply only to compacted conversation history. Lossless-claw does not supersede memory tools globally.",
|
|
147
203
|
"",
|
|
@@ -162,6 +218,27 @@ function snapshotPluginEnv(env: NodeJS.ProcessEnv = process.env): PluginEnvSnaps
|
|
|
162
218
|
};
|
|
163
219
|
}
|
|
164
220
|
|
|
221
|
+
/** Coerce a plugin-config-like value into a plain object when possible. */
|
|
222
|
+
function toPluginConfig(value: unknown): Record<string, unknown> | undefined {
|
|
223
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
224
|
+
? (value as Record<string, unknown>)
|
|
225
|
+
: undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Resolve plugin config from direct runtime injection or the root OpenClaw config fallback. */
|
|
229
|
+
function resolvePluginConfig(api: OpenClawPluginApi): Record<string, unknown> | undefined {
|
|
230
|
+
const directPluginConfig = toPluginConfig(api.pluginConfig);
|
|
231
|
+
if (directPluginConfig && Object.keys(directPluginConfig).length > 0) {
|
|
232
|
+
return directPluginConfig;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const rootConfig = toPluginConfig(api.config);
|
|
236
|
+
const plugins = toPluginConfig(rootConfig?.plugins);
|
|
237
|
+
const entries = toPluginConfig(plugins?.entries);
|
|
238
|
+
const pluginEntry = toPluginConfig(entries?.["lossless-claw"]);
|
|
239
|
+
return toPluginConfig(pluginEntry?.config);
|
|
240
|
+
}
|
|
241
|
+
|
|
165
242
|
function truncateErrorMessage(message: string, maxChars = 240): string {
|
|
166
243
|
return message.length <= maxChars ? message : `${message.slice(0, maxChars)}...`;
|
|
167
244
|
}
|
|
@@ -459,7 +536,7 @@ function normalizeProviderId(provider: string): string {
|
|
|
459
536
|
}
|
|
460
537
|
|
|
461
538
|
/** Resolve known provider API defaults when model lookup misses. */
|
|
462
|
-
function inferApiFromProvider(provider: string): string {
|
|
539
|
+
function inferApiFromProvider(provider: string): string | undefined {
|
|
463
540
|
const normalized = normalizeProviderId(provider);
|
|
464
541
|
const map: Record<string, string> = {
|
|
465
542
|
anthropic: "anthropic-messages",
|
|
@@ -472,7 +549,7 @@ function inferApiFromProvider(provider: string): string {
|
|
|
472
549
|
"google-vertex": "google-vertex",
|
|
473
550
|
"amazon-bedrock": "bedrock-converse-stream",
|
|
474
551
|
};
|
|
475
|
-
return map[normalized]
|
|
552
|
+
return map[normalized];
|
|
476
553
|
}
|
|
477
554
|
|
|
478
555
|
/** Codex Responses rejects `temperature`; omit it for that API family. */
|
|
@@ -562,12 +639,18 @@ function buildModelAuthLookupModel(params: {
|
|
|
562
639
|
provider: string;
|
|
563
640
|
model: string;
|
|
564
641
|
api?: string;
|
|
642
|
+
contextWindow?: number;
|
|
565
643
|
}): RuntimeModelAuthModel {
|
|
644
|
+
const contextWindow =
|
|
645
|
+
typeof params.contextWindow === "number" && Number.isFinite(params.contextWindow) && params.contextWindow > 0
|
|
646
|
+
? params.contextWindow
|
|
647
|
+
: 1_000_000;
|
|
648
|
+
|
|
566
649
|
return {
|
|
567
650
|
id: params.model,
|
|
568
651
|
name: params.model,
|
|
569
652
|
provider: params.provider,
|
|
570
|
-
api: params.api?.trim() || inferApiFromProvider(params.provider),
|
|
653
|
+
api: params.api?.trim() || inferApiFromProvider(params.provider) || "",
|
|
571
654
|
reasoning: false,
|
|
572
655
|
input: ["text"],
|
|
573
656
|
cost: {
|
|
@@ -576,7 +659,7 @@ function buildModelAuthLookupModel(params: {
|
|
|
576
659
|
cacheRead: 0,
|
|
577
660
|
cacheWrite: 0,
|
|
578
661
|
},
|
|
579
|
-
contextWindow
|
|
662
|
+
contextWindow,
|
|
580
663
|
maxTokens: 8_000,
|
|
581
664
|
};
|
|
582
665
|
}
|
|
@@ -587,6 +670,78 @@ function resolveApiKeyFromAuthResult(auth: RuntimeModelAuthResult | undefined):
|
|
|
587
670
|
return apiKey ? apiKey : undefined;
|
|
588
671
|
}
|
|
589
672
|
|
|
673
|
+
/** Normalize a runtime auth override base URL when present. */
|
|
674
|
+
function resolveBaseUrlFromAuthResult(auth: RuntimeModelAuthResult | undefined): string | undefined {
|
|
675
|
+
const baseUrl = auth?.baseUrl?.trim();
|
|
676
|
+
return baseUrl ? baseUrl : undefined;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Normalize raw runtime auth headers into plain string headers. */
|
|
680
|
+
function resolveRuntimeAuthHeaders(
|
|
681
|
+
request: RuntimeModelRequestTransportOverrides | undefined,
|
|
682
|
+
): Record<string, string> | undefined {
|
|
683
|
+
if (!request) {
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const headers: Record<string, string> = {};
|
|
688
|
+
if (isRecord(request.headers)) {
|
|
689
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
690
|
+
if (typeof value !== "string") {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const headerName = key.trim();
|
|
694
|
+
const headerValue = value.trim();
|
|
695
|
+
if (headerName && headerValue) {
|
|
696
|
+
headers[headerName] = headerValue;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const auth = request.auth;
|
|
702
|
+
if (auth?.mode === "authorization-bearer") {
|
|
703
|
+
const token = auth.token.trim();
|
|
704
|
+
if (token) {
|
|
705
|
+
for (const key of Object.keys(headers)) {
|
|
706
|
+
if (key.toLowerCase() === "authorization") {
|
|
707
|
+
delete headers[key];
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
headers.Authorization = `Bearer ${token}`;
|
|
711
|
+
}
|
|
712
|
+
} else if (auth?.mode === "header") {
|
|
713
|
+
const headerName = auth.headerName.trim();
|
|
714
|
+
const value = auth.value.trim();
|
|
715
|
+
if (headerName && value) {
|
|
716
|
+
const normalizedHeader = headerName.toLowerCase();
|
|
717
|
+
for (const key of Object.keys(headers)) {
|
|
718
|
+
if (
|
|
719
|
+
key.toLowerCase() === normalizedHeader ||
|
|
720
|
+
(normalizedHeader !== "authorization" && key.toLowerCase() === "authorization")
|
|
721
|
+
) {
|
|
722
|
+
delete headers[key];
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
headers[headerName] = `${auth.prefix?.trim() ?? ""}${value}`;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** Attach OpenClaw transport overrides to a model for runtimes that inspect the shared symbol. */
|
|
733
|
+
function attachRuntimeAuthRequestTransport<TModel extends object>(
|
|
734
|
+
model: TModel,
|
|
735
|
+
request: RuntimeModelRequestTransportOverrides | undefined,
|
|
736
|
+
): TModel {
|
|
737
|
+
if (!request) {
|
|
738
|
+
return model;
|
|
739
|
+
}
|
|
740
|
+
const next = { ...model } as TModel & Record<symbol, unknown>;
|
|
741
|
+
next[Symbol.for("openclaw.modelProviderRequestTransport")] = request;
|
|
742
|
+
return next;
|
|
743
|
+
}
|
|
744
|
+
|
|
590
745
|
function buildLegacyAuthFallbackWarning(): string {
|
|
591
746
|
return [
|
|
592
747
|
"[lcm] OpenClaw runtime.modelAuth is unavailable; using legacy auth-profiles fallback.",
|
|
@@ -1097,11 +1252,9 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1097
1252
|
envSnapshot.openclawDefaultModel = readDefaultModelFromConfig(api.config);
|
|
1098
1253
|
const modelAuth = getRuntimeModelAuth(api);
|
|
1099
1254
|
const readEnv: ReadEnvFn = (key) => process.env[key];
|
|
1100
|
-
const pluginConfig =
|
|
1101
|
-
api.pluginConfig && typeof api.pluginConfig === "object" && !Array.isArray(api.pluginConfig)
|
|
1102
|
-
? api.pluginConfig
|
|
1103
|
-
: undefined;
|
|
1255
|
+
const pluginConfig = resolvePluginConfig(api);
|
|
1104
1256
|
const config = resolveLcmConfig(process.env, pluginConfig);
|
|
1257
|
+
const log = createLcmLogger(api);
|
|
1105
1258
|
|
|
1106
1259
|
// Read model overrides from plugin config
|
|
1107
1260
|
if (pluginConfig) {
|
|
@@ -1116,7 +1269,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1116
1269
|
}
|
|
1117
1270
|
|
|
1118
1271
|
if (!modelAuth) {
|
|
1119
|
-
|
|
1272
|
+
log.warn(buildLegacyAuthFallbackWarning());
|
|
1120
1273
|
}
|
|
1121
1274
|
|
|
1122
1275
|
/** Resolve the best config object to hand to runtime.modelAuth for this lookup. */
|
|
@@ -1145,7 +1298,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1145
1298
|
try {
|
|
1146
1299
|
const modelAuthKey = resolveApiKeyFromAuthResult(
|
|
1147
1300
|
await modelAuth.getApiKeyForModel({
|
|
1148
|
-
model: buildModelAuthLookupModel({ provider, model }),
|
|
1301
|
+
model: buildModelAuthLookupModel({ provider, model, contextWindow: 1_000_000 }),
|
|
1149
1302
|
cfg: modelAuthConfig,
|
|
1150
1303
|
...(options?.profileId ? { profileId: options.profileId } : {}),
|
|
1151
1304
|
...(options?.preferredProfile ? { preferredProfile: options.preferredProfile } : {}),
|
|
@@ -1194,6 +1347,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1194
1347
|
authProfileId,
|
|
1195
1348
|
agentDir,
|
|
1196
1349
|
runtimeConfig,
|
|
1350
|
+
skipModelAuth,
|
|
1197
1351
|
messages,
|
|
1198
1352
|
system,
|
|
1199
1353
|
maxTokens,
|
|
@@ -1213,6 +1367,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1213
1367
|
if (!providerId || !modelId) {
|
|
1214
1368
|
return { content: [] };
|
|
1215
1369
|
}
|
|
1370
|
+
const workspaceDir = agentDir?.trim() || api.resolvePath(".");
|
|
1216
1371
|
|
|
1217
1372
|
// When runtimeConfig is undefined (e.g. resolveLargeFileTextSummarizer
|
|
1218
1373
|
// passes legacyParams without config), fall back to the plugin API so
|
|
@@ -1229,6 +1384,9 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1229
1384
|
const knownModel =
|
|
1230
1385
|
typeof mod.getModel === "function" ? mod.getModel(providerId, modelId) : undefined;
|
|
1231
1386
|
const fallbackApi =
|
|
1387
|
+
(isRecord(knownModel) && typeof knownModel.api === "string" && knownModel.api.trim()
|
|
1388
|
+
? knownModel.api.trim()
|
|
1389
|
+
: undefined) ||
|
|
1232
1390
|
providerApi?.trim() ||
|
|
1233
1391
|
resolveProviderApiFromRuntimeConfig(effectiveRuntimeConfig, providerId) ||
|
|
1234
1392
|
(() => {
|
|
@@ -1243,6 +1401,12 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1243
1401
|
return first.api.trim();
|
|
1244
1402
|
})() ||
|
|
1245
1403
|
inferApiFromProvider(providerId);
|
|
1404
|
+
if (!fallbackApi) {
|
|
1405
|
+
throw new Error(
|
|
1406
|
+
`[lcm] unable to resolve API family for provider ${providerId}; set models.providers.${providerId}.api explicitly instead of falling back implicitly.`,
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
const modelAuthConfig = resolveModelAuthConfig(effectiveRuntimeConfig);
|
|
1246
1410
|
|
|
1247
1411
|
// Resolve provider-level config (baseUrl, headers, etc.) from runtime config.
|
|
1248
1412
|
// Custom/proxy providers (e.g. bailian, local proxies) store their baseUrl and
|
|
@@ -1259,7 +1423,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1259
1423
|
return isRecord(cfg) ? cfg : {};
|
|
1260
1424
|
})();
|
|
1261
1425
|
|
|
1262
|
-
|
|
1426
|
+
let resolvedModel =
|
|
1263
1427
|
isRecord(knownModel) &&
|
|
1264
1428
|
typeof knownModel.api === "string" &&
|
|
1265
1429
|
typeof knownModel.provider === "string" &&
|
|
@@ -1268,18 +1432,29 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1268
1432
|
...knownModel,
|
|
1269
1433
|
id: knownModel.id,
|
|
1270
1434
|
provider: knownModel.provider,
|
|
1271
|
-
api:
|
|
1272
|
-
|
|
1435
|
+
api:
|
|
1436
|
+
typeof providerLevelConfig.api === "string" && providerLevelConfig.api.trim()
|
|
1437
|
+
? providerLevelConfig.api.trim()
|
|
1438
|
+
: knownModel.api,
|
|
1439
|
+
// Provider config must be able to override built-in transport defaults.
|
|
1440
|
+
// Otherwise built-in providers like `openai` keep their catalog baseUrl
|
|
1441
|
+
// (`https://api.openai.com/v1`) even when OpenClaw runtime config points
|
|
1442
|
+
// that provider id at a custom proxy.
|
|
1273
1443
|
// Always set baseUrl to a string — pi-ai's detectCompat() crashes when
|
|
1274
1444
|
// baseUrl is undefined.
|
|
1275
1445
|
baseUrl:
|
|
1276
|
-
typeof
|
|
1277
|
-
?
|
|
1278
|
-
: typeof
|
|
1279
|
-
?
|
|
1446
|
+
typeof providerLevelConfig.baseUrl === "string"
|
|
1447
|
+
? providerLevelConfig.baseUrl
|
|
1448
|
+
: typeof knownModel.baseUrl === "string"
|
|
1449
|
+
? knownModel.baseUrl
|
|
1280
1450
|
: "",
|
|
1281
|
-
...(
|
|
1282
|
-
? {
|
|
1451
|
+
...(isRecord(providerLevelConfig.headers)
|
|
1452
|
+
? {
|
|
1453
|
+
headers: {
|
|
1454
|
+
...(isRecord(knownModel.headers) ? knownModel.headers : {}),
|
|
1455
|
+
...providerLevelConfig.headers,
|
|
1456
|
+
},
|
|
1457
|
+
}
|
|
1283
1458
|
: {}),
|
|
1284
1459
|
}
|
|
1285
1460
|
: {
|
|
@@ -1295,7 +1470,7 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1295
1470
|
cacheRead: 0,
|
|
1296
1471
|
cacheWrite: 0,
|
|
1297
1472
|
},
|
|
1298
|
-
contextWindow:
|
|
1473
|
+
contextWindow: 1_000_000,
|
|
1299
1474
|
maxTokens: 8_000,
|
|
1300
1475
|
// Always set baseUrl to a string — pi-ai's detectCompat() crashes when
|
|
1301
1476
|
// baseUrl is undefined.
|
|
@@ -1307,8 +1482,51 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1307
1482
|
: {}),
|
|
1308
1483
|
};
|
|
1309
1484
|
|
|
1485
|
+
let runtimeAuth: RuntimeModelAuthResult | undefined;
|
|
1486
|
+
if (modelAuth && skipModelAuth !== true && typeof modelAuth.getRuntimeAuthForModel === "function") {
|
|
1487
|
+
try {
|
|
1488
|
+
runtimeAuth = await modelAuth.getRuntimeAuthForModel({
|
|
1489
|
+
model: buildModelAuthLookupModel({
|
|
1490
|
+
provider: providerId,
|
|
1491
|
+
model: modelId,
|
|
1492
|
+
api: resolvedModel.api,
|
|
1493
|
+
contextWindow: resolvedModel.contextWindow,
|
|
1494
|
+
}),
|
|
1495
|
+
cfg: modelAuthConfig,
|
|
1496
|
+
...(authProfileId ? { profileId: authProfileId } : {}),
|
|
1497
|
+
workspaceDir,
|
|
1498
|
+
});
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
console.error(
|
|
1501
|
+
`[lcm] modelAuth.getRuntimeAuthForModel FAILED:`,
|
|
1502
|
+
err instanceof Error ? err.message : err,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const runtimeAuthBaseUrl = resolveBaseUrlFromAuthResult(runtimeAuth);
|
|
1508
|
+
const runtimeAuthHeaders = resolveRuntimeAuthHeaders(runtimeAuth?.request);
|
|
1509
|
+
resolvedModel = attachRuntimeAuthRequestTransport(
|
|
1510
|
+
{
|
|
1511
|
+
...resolvedModel,
|
|
1512
|
+
...(runtimeAuthBaseUrl ? { baseUrl: runtimeAuthBaseUrl } : {}),
|
|
1513
|
+
...(runtimeAuthHeaders
|
|
1514
|
+
? {
|
|
1515
|
+
headers: {
|
|
1516
|
+
...(isRecord(resolvedModel.headers) ? resolvedModel.headers : {}),
|
|
1517
|
+
...runtimeAuthHeaders,
|
|
1518
|
+
},
|
|
1519
|
+
}
|
|
1520
|
+
: {}),
|
|
1521
|
+
},
|
|
1522
|
+
runtimeAuth?.request,
|
|
1523
|
+
);
|
|
1524
|
+
|
|
1310
1525
|
let resolvedApiKey = apiKey?.trim();
|
|
1311
|
-
if (!resolvedApiKey
|
|
1526
|
+
if (!resolvedApiKey) {
|
|
1527
|
+
resolvedApiKey = resolveApiKeyFromAuthResult(runtimeAuth);
|
|
1528
|
+
}
|
|
1529
|
+
if (!resolvedApiKey && modelAuth && skipModelAuth !== true) {
|
|
1312
1530
|
try {
|
|
1313
1531
|
resolvedApiKey = resolveApiKeyFromAuthResult(
|
|
1314
1532
|
await modelAuth.getApiKeyForModel({
|
|
@@ -1316,32 +1534,27 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1316
1534
|
provider: providerId,
|
|
1317
1535
|
model: modelId,
|
|
1318
1536
|
api: resolvedModel.api,
|
|
1537
|
+
contextWindow: resolvedModel.contextWindow,
|
|
1319
1538
|
}),
|
|
1320
|
-
cfg:
|
|
1539
|
+
cfg: modelAuthConfig,
|
|
1321
1540
|
...(authProfileId ? { profileId: authProfileId } : {}),
|
|
1322
1541
|
}),
|
|
1323
1542
|
);
|
|
1324
1543
|
} catch (err) {
|
|
1325
|
-
|
|
1326
|
-
`[lcm] modelAuth.getApiKeyForModel FAILED:`,
|
|
1327
|
-
err instanceof Error ? err.message : err,
|
|
1328
|
-
);
|
|
1544
|
+
log.warn(`[lcm] modelAuth.getApiKeyForModel FAILED: ${describeLogError(err)}`);
|
|
1329
1545
|
}
|
|
1330
1546
|
}
|
|
1331
|
-
if (!resolvedApiKey && modelAuth) {
|
|
1547
|
+
if (!resolvedApiKey && modelAuth && skipModelAuth !== true) {
|
|
1332
1548
|
try {
|
|
1333
1549
|
resolvedApiKey = resolveApiKeyFromAuthResult(
|
|
1334
1550
|
await modelAuth.resolveApiKeyForProvider({
|
|
1335
1551
|
provider: providerId,
|
|
1336
|
-
cfg:
|
|
1552
|
+
cfg: modelAuthConfig,
|
|
1337
1553
|
...(authProfileId ? { profileId: authProfileId } : {}),
|
|
1338
1554
|
}),
|
|
1339
1555
|
);
|
|
1340
1556
|
} catch (err) {
|
|
1341
|
-
|
|
1342
|
-
`[lcm] modelAuth.resolveApiKeyForProvider FAILED:`,
|
|
1343
|
-
err instanceof Error ? err.message : err,
|
|
1344
|
-
);
|
|
1557
|
+
log.warn(`[lcm] modelAuth.resolveApiKeyForProvider FAILED: ${describeLogError(err)}`);
|
|
1345
1558
|
}
|
|
1346
1559
|
}
|
|
1347
1560
|
if (!resolvedApiKey) {
|
|
@@ -1426,11 +1639,21 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1426
1639
|
...requestMetadata,
|
|
1427
1640
|
};
|
|
1428
1641
|
} catch (err) {
|
|
1429
|
-
|
|
1642
|
+
log.error(`[lcm] completeSimple error: ${describeLogError(err)}`);
|
|
1430
1643
|
const authError = detectProviderAuthError(err);
|
|
1644
|
+
const configError =
|
|
1645
|
+
!authError &&
|
|
1646
|
+
err instanceof Error &&
|
|
1647
|
+
err.message.startsWith(PROVIDER_API_RESOLUTION_ERROR_PREFIX)
|
|
1648
|
+
? {
|
|
1649
|
+
kind: "provider_config",
|
|
1650
|
+
message: err.message,
|
|
1651
|
+
}
|
|
1652
|
+
: undefined;
|
|
1431
1653
|
return {
|
|
1432
1654
|
content: [],
|
|
1433
1655
|
...(authError ? { error: authError } : {}),
|
|
1656
|
+
...(configError ? { error: configError } : {}),
|
|
1434
1657
|
};
|
|
1435
1658
|
}
|
|
1436
1659
|
},
|
|
@@ -1536,15 +1759,66 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1536
1759
|
}
|
|
1537
1760
|
},
|
|
1538
1761
|
agentLaneSubagent: "subagent",
|
|
1539
|
-
log
|
|
1540
|
-
info: (msg) => console.error(msg),
|
|
1541
|
-
warn: (msg) => console.error(msg),
|
|
1542
|
-
error: (msg) => console.error(msg),
|
|
1543
|
-
debug: (msg) => api.logger.debug?.(msg),
|
|
1544
|
-
},
|
|
1762
|
+
log,
|
|
1545
1763
|
};
|
|
1546
1764
|
}
|
|
1547
1765
|
|
|
1766
|
+
/**
|
|
1767
|
+
* Wire event handlers, context engines, tools, and commands to the
|
|
1768
|
+
* OpenClaw plugin API using shared init closures.
|
|
1769
|
+
*/
|
|
1770
|
+
function wirePluginHandlers(
|
|
1771
|
+
api: OpenClawPluginApi,
|
|
1772
|
+
deps: LcmDependencies,
|
|
1773
|
+
shared: SharedLcmInit,
|
|
1774
|
+
): void {
|
|
1775
|
+
api.on("before_reset", async (event, ctx) => {
|
|
1776
|
+
await (await shared.waitForEngine()).handleBeforeReset({
|
|
1777
|
+
reason: event.reason,
|
|
1778
|
+
sessionId: ctx.sessionId,
|
|
1779
|
+
sessionKey: ctx.sessionKey,
|
|
1780
|
+
});
|
|
1781
|
+
});
|
|
1782
|
+
api.on("before_prompt_build", () => ({
|
|
1783
|
+
prependSystemContext: LOSSLESS_RECALL_POLICY_PROMPT,
|
|
1784
|
+
}));
|
|
1785
|
+
api.on("session_end", async (event) => {
|
|
1786
|
+
const lifecycleEvent = event as SessionEndLifecycleEvent;
|
|
1787
|
+
await (await shared.waitForEngine()).handleSessionEnd({
|
|
1788
|
+
reason: lifecycleEvent.reason,
|
|
1789
|
+
sessionId: lifecycleEvent.sessionId,
|
|
1790
|
+
sessionKey: lifecycleEvent.sessionKey,
|
|
1791
|
+
nextSessionId: lifecycleEvent.nextSessionId,
|
|
1792
|
+
nextSessionKey: lifecycleEvent.nextSessionKey,
|
|
1793
|
+
});
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
api.registerContextEngine("lossless-claw", () => shared.getCachedEngine() ?? shared.waitForEngine());
|
|
1797
|
+
api.registerContextEngine("default", () => shared.getCachedEngine() ?? shared.waitForEngine());
|
|
1798
|
+
|
|
1799
|
+
api.registerTool((ctx) =>
|
|
1800
|
+
createLcmGrepTool({ deps, getLcm: shared.waitForEngine, sessionKey: ctx.sessionKey }),
|
|
1801
|
+
);
|
|
1802
|
+
api.registerTool((ctx) =>
|
|
1803
|
+
createLcmDescribeTool({ deps, getLcm: shared.waitForEngine, sessionKey: ctx.sessionKey }),
|
|
1804
|
+
);
|
|
1805
|
+
api.registerTool((ctx) =>
|
|
1806
|
+
createLcmExpandTool({ deps, getLcm: shared.waitForEngine, sessionKey: ctx.sessionKey }),
|
|
1807
|
+
);
|
|
1808
|
+
api.registerTool((ctx) =>
|
|
1809
|
+
createLcmExpandQueryTool({
|
|
1810
|
+
deps,
|
|
1811
|
+
getLcm: shared.waitForEngine,
|
|
1812
|
+
sessionKey: ctx.sessionKey,
|
|
1813
|
+
requesterSessionKey: ctx.sessionKey,
|
|
1814
|
+
}),
|
|
1815
|
+
);
|
|
1816
|
+
|
|
1817
|
+
api.registerCommand(
|
|
1818
|
+
createLcmCommand({ db: shared.waitForDatabase, config: deps.config, deps }),
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1548
1822
|
const lcmPlugin = {
|
|
1549
1823
|
id: "lossless-claw",
|
|
1550
1824
|
name: "Lossless Context Management",
|
|
@@ -1563,82 +1837,200 @@ const lcmPlugin = {
|
|
|
1563
1837
|
|
|
1564
1838
|
register(api: OpenClawPluginApi) {
|
|
1565
1839
|
const deps = createLcmDependencies(api);
|
|
1566
|
-
const
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1840
|
+
const dbPath = deps.config.databasePath;
|
|
1841
|
+
const normalizedDbPath = normalizePath(dbPath);
|
|
1842
|
+
|
|
1843
|
+
// ── Singleton check ─────────────────────────────────────────────
|
|
1844
|
+
// OpenClaw v2026.4.5+ calls register() per-agent-context (main,
|
|
1845
|
+
// subagents, cron lanes). Reuse the existing connection and engine
|
|
1846
|
+
// when the same DB path is already initialized.
|
|
1847
|
+
const existingInit = getSharedInit(normalizedDbPath);
|
|
1848
|
+
if (existingInit && !existingInit.stopped) {
|
|
1849
|
+
deps.log.info(`[lcm] Reusing shared engine init for db=${normalizedDbPath}`);
|
|
1850
|
+
wirePluginHandlers(api, deps, existingInit);
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// ── Eager-first DB init with deferred fallback on lock ──────────
|
|
1855
|
+
let database: DatabaseSync | null = null;
|
|
1856
|
+
let lcm: LcmContextEngine | null = null;
|
|
1857
|
+
let initPromise: Promise<LcmContextEngine> | null = null;
|
|
1858
|
+
let initError: Error | null = null;
|
|
1859
|
+
let resolveDeferredInit: ((engine: LcmContextEngine) => void) | null = null;
|
|
1860
|
+
let rejectDeferredInit: ((error: Error) => void) | null = null;
|
|
1861
|
+
let stopped = false;
|
|
1862
|
+
|
|
1863
|
+
/** Normalize unknown failures into stable Error instances. */
|
|
1864
|
+
function toInitError(error: unknown): Error {
|
|
1865
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/** Build a live DB+engine pair and roll back the DB handle if engine init fails. */
|
|
1869
|
+
function initializeEngine(): LcmContextEngine {
|
|
1870
|
+
const startedAt = Date.now();
|
|
1871
|
+
const nextDatabase = createLcmDatabaseConnection(dbPath);
|
|
1872
|
+
try {
|
|
1873
|
+
const nextEngine = new LcmContextEngine(deps, nextDatabase);
|
|
1874
|
+
database = nextDatabase;
|
|
1875
|
+
lcm = nextEngine;
|
|
1876
|
+
initError = null;
|
|
1877
|
+
deps.log.info(
|
|
1878
|
+
`[lcm] Engine initialized for db=${normalizedDbPath} duration=${Date.now() - startedAt}ms`,
|
|
1879
|
+
);
|
|
1880
|
+
return nextEngine;
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
closeLcmConnection(nextDatabase);
|
|
1883
|
+
deps.log.info(
|
|
1884
|
+
`[lcm] Engine init failed for db=${normalizedDbPath} duration=${Date.now() - startedAt}ms error=${toInitError(error).message}`,
|
|
1885
|
+
);
|
|
1886
|
+
throw error;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/** Keep one shared deferred init promise so early callers all await the same retry. */
|
|
1891
|
+
function ensureDeferredInitPromise(): Promise<LcmContextEngine> {
|
|
1892
|
+
if (initPromise) {
|
|
1893
|
+
return initPromise;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
initPromise = new Promise<LcmContextEngine>((resolve, reject) => {
|
|
1897
|
+
resolveDeferredInit = resolve;
|
|
1898
|
+
rejectDeferredInit = reject;
|
|
1574
1899
|
});
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1900
|
+
initPromise.catch(() => {});
|
|
1901
|
+
return initPromise;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
/** Resolve the shared deferred init promise exactly once. */
|
|
1905
|
+
function resolveDeferredEngine(nextEngine: LcmContextEngine): void {
|
|
1906
|
+
const resolve = resolveDeferredInit;
|
|
1907
|
+
resolveDeferredInit = null;
|
|
1908
|
+
rejectDeferredInit = null;
|
|
1909
|
+
resolve?.(nextEngine);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/** Reject the shared deferred init promise exactly once and retain the root cause. */
|
|
1913
|
+
function rejectDeferredEngine(error: Error): void {
|
|
1914
|
+
initError = error;
|
|
1915
|
+
const reject = rejectDeferredInit;
|
|
1916
|
+
resolveDeferredInit = null;
|
|
1917
|
+
rejectDeferredInit = null;
|
|
1918
|
+
reject?.(error);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/** Return the initialized engine, waiting for deferred startup when the DB is lock-contended. */
|
|
1922
|
+
async function waitForEngine(): Promise<LcmContextEngine> {
|
|
1923
|
+
if (stopped) {
|
|
1924
|
+
throw new Error("[lcm] Database connection closed after gateway_stop");
|
|
1925
|
+
}
|
|
1926
|
+
if (initError) {
|
|
1927
|
+
throw initError;
|
|
1928
|
+
}
|
|
1929
|
+
if (lcm) {
|
|
1930
|
+
return lcm;
|
|
1931
|
+
}
|
|
1932
|
+
if (initPromise) {
|
|
1933
|
+
return initPromise;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
try {
|
|
1937
|
+
const nextEngine = initializeEngine();
|
|
1938
|
+
initPromise = Promise.resolve(nextEngine);
|
|
1939
|
+
return nextEngine;
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
const normalized = toInitError(error);
|
|
1942
|
+
if (!/database is locked/i.test(normalized.message)) {
|
|
1943
|
+
initError = normalized;
|
|
1944
|
+
throw normalized;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
deps.log.warn("[lcm] DB locked during eager init, deferring to gateway_start");
|
|
1948
|
+
return ensureDeferredInitPromise();
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
/** Return the initialized DB handle, sharing the same wait/error semantics as the engine. */
|
|
1953
|
+
async function waitForDatabase(): Promise<DatabaseSync> {
|
|
1954
|
+
await waitForEngine();
|
|
1955
|
+
if (!database) {
|
|
1956
|
+
throw initError ?? new Error("[lcm] Database initialization finished without a handle");
|
|
1957
|
+
}
|
|
1958
|
+
return database;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
try {
|
|
1962
|
+
const nextEngine = initializeEngine();
|
|
1963
|
+
initPromise = Promise.resolve(nextEngine);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
const normalized = toInitError(error);
|
|
1966
|
+
if (!/database is locked/i.test(normalized.message)) {
|
|
1967
|
+
initError = normalized;
|
|
1968
|
+
throw normalized;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
deps.log.warn("[lcm] DB locked during eager init, deferring to gateway_start");
|
|
1972
|
+
ensureDeferredInitPromise();
|
|
1973
|
+
api.on("gateway_start", async () => {
|
|
1974
|
+
if (stopped || lcm || initError) {
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
const nextEngine = initializeEngine();
|
|
1979
|
+
initPromise = Promise.resolve(nextEngine);
|
|
1980
|
+
resolveDeferredEngine(nextEngine);
|
|
1981
|
+
} catch (retryError) {
|
|
1982
|
+
const normalizedRetryError = toInitError(retryError);
|
|
1983
|
+
rejectDeferredEngine(normalizedRetryError);
|
|
1984
|
+
deps.log.error(`[lcm] Deferred DB init failed: ${normalizedRetryError.message}`);
|
|
1985
|
+
}
|
|
1587
1986
|
});
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
const shared: SharedLcmInit = {
|
|
1990
|
+
stopped: false,
|
|
1991
|
+
getCachedEngine: () => lcm,
|
|
1992
|
+
waitForEngine,
|
|
1993
|
+
waitForDatabase,
|
|
1994
|
+
};
|
|
1995
|
+
setSharedInit(normalizedDbPath, shared);
|
|
1996
|
+
|
|
1997
|
+
api.on("gateway_stop", async () => {
|
|
1998
|
+
stopped = true;
|
|
1999
|
+
shared.stopped = true;
|
|
2000
|
+
if (!lcm && !database) {
|
|
2001
|
+
rejectDeferredEngine(new Error("[lcm] Database connection closed after gateway_stop"));
|
|
2002
|
+
}
|
|
2003
|
+
if (database) {
|
|
2004
|
+
closeLcmConnection(database);
|
|
2005
|
+
database = null;
|
|
2006
|
+
}
|
|
2007
|
+
lcm = null;
|
|
2008
|
+
removeSharedInit(normalizedDbPath);
|
|
1588
2009
|
});
|
|
1589
|
-
|
|
1590
|
-
api
|
|
1591
|
-
api.registerTool((ctx) =>
|
|
1592
|
-
createLcmGrepTool({
|
|
1593
|
-
deps,
|
|
1594
|
-
lcm,
|
|
1595
|
-
sessionKey: ctx.sessionKey,
|
|
1596
|
-
}),
|
|
1597
|
-
);
|
|
1598
|
-
api.registerTool((ctx) =>
|
|
1599
|
-
createLcmDescribeTool({
|
|
1600
|
-
deps,
|
|
1601
|
-
lcm,
|
|
1602
|
-
sessionKey: ctx.sessionKey,
|
|
1603
|
-
}),
|
|
1604
|
-
);
|
|
1605
|
-
api.registerTool((ctx) =>
|
|
1606
|
-
createLcmExpandTool({
|
|
1607
|
-
deps,
|
|
1608
|
-
lcm,
|
|
1609
|
-
sessionKey: ctx.sessionKey,
|
|
1610
|
-
}),
|
|
1611
|
-
);
|
|
1612
|
-
api.registerTool((ctx) =>
|
|
1613
|
-
createLcmExpandQueryTool({
|
|
1614
|
-
deps,
|
|
1615
|
-
lcm,
|
|
1616
|
-
sessionKey: ctx.sessionKey,
|
|
1617
|
-
requesterSessionKey: ctx.sessionKey,
|
|
1618
|
-
}),
|
|
1619
|
-
);
|
|
1620
|
-
api.registerCommand(
|
|
1621
|
-
createLcmCommand({
|
|
1622
|
-
db: database,
|
|
1623
|
-
config: deps.config,
|
|
1624
|
-
deps,
|
|
1625
|
-
}),
|
|
1626
|
-
);
|
|
2010
|
+
|
|
2011
|
+
wirePluginHandlers(api, deps, shared);
|
|
1627
2012
|
|
|
1628
2013
|
logStartupBannerOnce({
|
|
1629
2014
|
key: "plugin-loaded",
|
|
1630
|
-
log: (message) =>
|
|
2015
|
+
log: (message) => deps.log.info(message),
|
|
1631
2016
|
message: `[lcm] Plugin loaded (enabled=${deps.config.enabled}, db=${deps.config.databasePath}, threshold=${deps.config.contextThreshold})`,
|
|
1632
2017
|
});
|
|
1633
2018
|
logStartupBannerOnce({
|
|
1634
2019
|
key: "compaction-model",
|
|
1635
|
-
log: (message) =>
|
|
2020
|
+
log: (message) => deps.log.info(message),
|
|
1636
2021
|
message: buildCompactionModelLog({
|
|
1637
2022
|
config: deps.config,
|
|
1638
2023
|
openClawConfig: api.config,
|
|
1639
2024
|
defaultProvider: process.env.OPENCLAW_PROVIDER?.trim() ?? "",
|
|
1640
2025
|
}),
|
|
1641
2026
|
});
|
|
2027
|
+
if (deps.config.fallbackProviders.length > 0) {
|
|
2028
|
+
logStartupBannerOnce({
|
|
2029
|
+
key: "fallback-providers",
|
|
2030
|
+
log: (message) => deps.log.info(message),
|
|
2031
|
+
message: `[lcm] Fallback providers: ${deps.config.fallbackProviders.map((fp) => `${fp.provider}/${fp.model}`).join(", ")}`,
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
1642
2034
|
},
|
|
1643
2035
|
};
|
|
1644
2036
|
|