@martian-engineering/lossless-claw 0.6.2 → 0.7.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 +16 -4
- package/docs/agent-tools.md +7 -1
- package/docs/configuration.md +200 -200
- package/openclaw.plugin.json +123 -0
- package/package.json +1 -1
- package/skills/lossless-claw/references/config.md +135 -3
- package/src/assembler.ts +5 -1
- package/src/compaction.ts +149 -38
- package/src/db/config.ts +102 -4
- package/src/db/connection.ts +20 -2
- package/src/db/migration.ts +57 -0
- package/src/engine.ts +980 -122
- package/src/lcm-log.ts +37 -0
- package/src/plugin/index.ts +407 -74
- package/src/plugin/lcm-command.ts +10 -4
- package/src/plugin/shared-init.ts +59 -0
- package/src/prune.ts +391 -0
- package/src/retrieval.ts +7 -5
- 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 +54 -30
- package/src/tools/lcm-describe-tool.ts +9 -4
- package/src/tools/lcm-expand-query-tool.ts +11 -6
- package/src/tools/lcm-expand-tool.ts +9 -4
- package/src/tools/lcm-grep-tool.ts +22 -8
- package/src/types.ts +1 -0
|
@@ -4,6 +4,7 @@ import { withDatabaseTransaction } from "../transaction-mutex.js";
|
|
|
4
4
|
import { sanitizeFts5Query } from "./fts5-sanitize.js";
|
|
5
5
|
import { buildLikeSearchPlan, containsCjk, createFallbackSnippet } from "./full-text-fallback.js";
|
|
6
6
|
import { parseUtcTimestamp, parseUtcTimestampOrNull } from "./parse-utc-timestamp.js";
|
|
7
|
+
import { buildFtsOrderBy, type SearchSort } from "./full-text-sort.js";
|
|
7
8
|
|
|
8
9
|
export type ConversationId = number;
|
|
9
10
|
export type MessageId = number;
|
|
@@ -94,6 +95,7 @@ export type MessageSearchInput = {
|
|
|
94
95
|
since?: Date;
|
|
95
96
|
before?: Date;
|
|
96
97
|
limit?: number;
|
|
98
|
+
sort?: SearchSort;
|
|
97
99
|
};
|
|
98
100
|
|
|
99
101
|
export type MessageSearchResult = {
|
|
@@ -714,6 +716,7 @@ export class ConversationStore {
|
|
|
714
716
|
input.conversationId,
|
|
715
717
|
input.since,
|
|
716
718
|
input.before,
|
|
719
|
+
input.sort,
|
|
717
720
|
);
|
|
718
721
|
} catch {
|
|
719
722
|
return this.searchLike(
|
|
@@ -764,6 +767,7 @@ export class ConversationStore {
|
|
|
764
767
|
conversationId?: ConversationId,
|
|
765
768
|
since?: Date,
|
|
766
769
|
before?: Date,
|
|
770
|
+
sort?: SearchSort,
|
|
767
771
|
): MessageSearchResult[] {
|
|
768
772
|
const where: string[] = ["messages_fts MATCH ?"];
|
|
769
773
|
const args: Array<string | number> = [sanitizeFts5Query(query)];
|
|
@@ -780,6 +784,7 @@ export class ConversationStore {
|
|
|
780
784
|
args.push(before.toISOString());
|
|
781
785
|
}
|
|
782
786
|
args.push(limit);
|
|
787
|
+
const orderBy = buildFtsOrderBy(sort, "m.created_at");
|
|
783
788
|
|
|
784
789
|
const sql = `SELECT
|
|
785
790
|
m.message_id,
|
|
@@ -791,7 +796,7 @@ export class ConversationStore {
|
|
|
791
796
|
FROM messages_fts
|
|
792
797
|
JOIN messages m ON m.message_id = messages_fts.rowid
|
|
793
798
|
WHERE ${where.join(" AND ")}
|
|
794
|
-
ORDER BY
|
|
799
|
+
ORDER BY ${orderBy}
|
|
795
800
|
LIMIT ?`;
|
|
796
801
|
const rows = this.db.prepare(sql).all(...args) as unknown as MessageSearchRow[];
|
|
797
802
|
return rows.map(toSearchResult);
|
|
@@ -21,9 +21,30 @@
|
|
|
21
21
|
* 'hello "world"' → '"hello" "world"'
|
|
22
22
|
*/
|
|
23
23
|
export function sanitizeFts5Query(raw: string): string {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// Preserve user-quoted phrases: extract "..." groups first, then tokenize the rest.
|
|
25
|
+
const parts: string[] = [];
|
|
26
|
+
const phraseRegex = /"([^"]+)"/g;
|
|
27
|
+
let match: RegExpExecArray | null;
|
|
28
|
+
let lastIndex = 0;
|
|
29
|
+
|
|
30
|
+
while ((match = phraseRegex.exec(raw)) !== null) {
|
|
31
|
+
// Process unquoted text before this phrase
|
|
32
|
+
const before = raw.slice(lastIndex, match.index);
|
|
33
|
+
for (const t of before.split(/\s+/).filter(Boolean)) {
|
|
34
|
+
parts.push(`"${t.replace(/"/g, "")}"`);
|
|
35
|
+
}
|
|
36
|
+
// Preserve the phrase as-is (strip internal quotes for safety)
|
|
37
|
+
const phrase = match[1].replace(/"/g, "").trim();
|
|
38
|
+
if (phrase) {
|
|
39
|
+
parts.push(`"${phrase}"`);
|
|
40
|
+
}
|
|
41
|
+
lastIndex = match.index + match[0].length;
|
|
27
42
|
}
|
|
28
|
-
|
|
43
|
+
|
|
44
|
+
// Process unquoted text after last phrase
|
|
45
|
+
for (const t of raw.slice(lastIndex).split(/\s+/).filter(Boolean)) {
|
|
46
|
+
parts.push(`"${t.replace(/"/g, "")}"`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parts.length > 0 ? parts.join(" ") : '""';
|
|
29
50
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SearchSort = "recency" | "relevance" | "hybrid";
|
|
2
|
+
|
|
3
|
+
export const AGE_DECAY_RATE = 0.001;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build the ORDER BY clause for FTS5-backed searches.
|
|
7
|
+
*
|
|
8
|
+
* `rank` is FTS5's BM25 score where lower (more negative) is better.
|
|
9
|
+
* `hybrid` keeps that relevance signal but applies a mild age penalty before
|
|
10
|
+
* LIMIT is enforced so older strong matches can still surface.
|
|
11
|
+
*/
|
|
12
|
+
export function buildFtsOrderBy(sort: SearchSort | undefined, createdAtExpr: string): string {
|
|
13
|
+
switch (sort ?? "recency") {
|
|
14
|
+
case "relevance":
|
|
15
|
+
return `rank ASC, ${createdAtExpr} DESC`;
|
|
16
|
+
case "hybrid":
|
|
17
|
+
return `(rank / (1 + ((julianday('now') - julianday(${createdAtExpr})) * 24 * ${AGE_DECAY_RATE}))) ASC, ${createdAtExpr} DESC`;
|
|
18
|
+
default:
|
|
19
|
+
return `${createdAtExpr} DESC`;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/store/index.ts
CHANGED
|
@@ -29,3 +29,11 @@ export type {
|
|
|
29
29
|
UpsertConversationBootstrapStateInput,
|
|
30
30
|
ConversationBootstrapStateRecord,
|
|
31
31
|
} from "./summary-store.js";
|
|
32
|
+
|
|
33
|
+
export { CompactionTelemetryStore } from "./compaction-telemetry-store.js";
|
|
34
|
+
export type {
|
|
35
|
+
CacheState,
|
|
36
|
+
ActivityBand,
|
|
37
|
+
ConversationCompactionTelemetryRecord,
|
|
38
|
+
UpsertConversationCompactionTelemetryInput,
|
|
39
|
+
} from "./compaction-telemetry-store.js";
|
|
@@ -3,6 +3,7 @@ import { withDatabaseTransaction } from "../transaction-mutex.js";
|
|
|
3
3
|
import { sanitizeFts5Query } from "./fts5-sanitize.js";
|
|
4
4
|
import { buildLikeSearchPlan, containsCjk, createFallbackSnippet } from "./full-text-fallback.js";
|
|
5
5
|
import { parseUtcTimestamp, parseUtcTimestampOrNull } from "./parse-utc-timestamp.js";
|
|
6
|
+
import { buildFtsOrderBy, type SearchSort } from "./full-text-sort.js";
|
|
6
7
|
|
|
7
8
|
export type SummaryKind = "leaf" | "condensed";
|
|
8
9
|
export type ContextItemType = "message" | "summary";
|
|
@@ -68,6 +69,7 @@ export type SummarySearchInput = {
|
|
|
68
69
|
since?: Date;
|
|
69
70
|
before?: Date;
|
|
70
71
|
limit?: number;
|
|
72
|
+
sort?: SearchSort;
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
export type SummarySearchResult = {
|
|
@@ -958,7 +960,10 @@ export class SummaryStore {
|
|
|
958
960
|
.run(conversationId, startOrdinal, summaryId);
|
|
959
961
|
|
|
960
962
|
// 3. Resequence all ordinals to maintain contiguity (no gaps).
|
|
961
|
-
//
|
|
963
|
+
// Pre-compute ranks from a SELECT (safe snapshot), then apply
|
|
964
|
+
// via 2-pass UPDATE loop using negative temps to avoid UNIQUE
|
|
965
|
+
// constraint violations. The SELECT reads post-delete/insert
|
|
966
|
+
// state and provides a consistent snapshot for resequencing.
|
|
962
967
|
const items = this.db
|
|
963
968
|
.prepare(
|
|
964
969
|
`SELECT ordinal FROM context_items
|
|
@@ -967,18 +972,17 @@ export class SummaryStore {
|
|
|
967
972
|
)
|
|
968
973
|
.all(conversationId) as unknown as { ordinal: number }[];
|
|
969
974
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
updateStmt.run(i, conversationId, -(i + 1));
|
|
975
|
+
if (items.length > 0 && items.some((item, i) => item.ordinal !== i)) {
|
|
976
|
+
const updateStmt = this.db.prepare(
|
|
977
|
+
`UPDATE context_items SET ordinal = ?
|
|
978
|
+
WHERE conversation_id = ? AND ordinal = ?`,
|
|
979
|
+
);
|
|
980
|
+
for (let i = 0; i < items.length; i++) {
|
|
981
|
+
updateStmt.run(-(i + 1), conversationId, items[i].ordinal);
|
|
982
|
+
}
|
|
983
|
+
for (let i = 0; i < items.length; i++) {
|
|
984
|
+
updateStmt.run(i, conversationId, -(i + 1));
|
|
985
|
+
}
|
|
982
986
|
}
|
|
983
987
|
}
|
|
984
988
|
|
|
@@ -1051,6 +1055,7 @@ export class SummaryStore {
|
|
|
1051
1055
|
input.conversationId,
|
|
1052
1056
|
input.since,
|
|
1053
1057
|
input.before,
|
|
1058
|
+
input.sort,
|
|
1054
1059
|
);
|
|
1055
1060
|
} catch {
|
|
1056
1061
|
return this.searchLike(
|
|
@@ -1073,6 +1078,7 @@ export class SummaryStore {
|
|
|
1073
1078
|
conversationId?: number,
|
|
1074
1079
|
since?: Date,
|
|
1075
1080
|
before?: Date,
|
|
1081
|
+
sort?: SearchSort,
|
|
1076
1082
|
): SummarySearchResult[] {
|
|
1077
1083
|
const where: string[] = ["summaries_fts MATCH ?"];
|
|
1078
1084
|
const args: Array<string | number> = [sanitizeFts5Query(query)];
|
|
@@ -1089,6 +1095,7 @@ export class SummaryStore {
|
|
|
1089
1095
|
args.push(before.toISOString());
|
|
1090
1096
|
}
|
|
1091
1097
|
args.push(limit);
|
|
1098
|
+
const orderBy = buildFtsOrderBy(sort, "s.created_at");
|
|
1092
1099
|
|
|
1093
1100
|
const sql = `SELECT
|
|
1094
1101
|
summaries_fts.summary_id,
|
|
@@ -1100,7 +1107,7 @@ export class SummaryStore {
|
|
|
1100
1107
|
FROM summaries_fts
|
|
1101
1108
|
JOIN summaries s ON s.summary_id = summaries_fts.summary_id
|
|
1102
1109
|
WHERE ${where.join(" AND ")}
|
|
1103
|
-
ORDER BY
|
|
1110
|
+
ORDER BY ${orderBy}
|
|
1104
1111
|
LIMIT ?`;
|
|
1105
1112
|
const rows = this.db.prepare(sql).all(...args) as unknown as SummarySearchRow[];
|
|
1106
1113
|
return rows.map(toSearchResult);
|
package/src/summarize.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeLogError } from "./lcm-log.js";
|
|
1
2
|
import type { LcmDependencies } from "./types.js";
|
|
2
3
|
|
|
3
4
|
export type LcmSummarizeOptions = {
|
|
@@ -1068,6 +1069,17 @@ function resolveSummaryCandidates(params: {
|
|
|
1068
1069
|
},
|
|
1069
1070
|
];
|
|
1070
1071
|
|
|
1072
|
+
// Append explicit fallback providers from config.
|
|
1073
|
+
for (const fb of params.deps.config.fallbackProviders ?? []) {
|
|
1074
|
+
resolutionCandidates.push({
|
|
1075
|
+
levelName: `explicit fallback (${fb.provider}/${fb.model})`,
|
|
1076
|
+
modelRef: `${fb.provider}/${fb.model}`,
|
|
1077
|
+
providerHint: fb.provider,
|
|
1078
|
+
hasExplicitProvider: true,
|
|
1079
|
+
useLegacyAuthProfile: false,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1071
1083
|
const resolvedCandidates: ResolvedSummaryCandidate[] = [];
|
|
1072
1084
|
for (const candidate of resolutionCandidates) {
|
|
1073
1085
|
if (!candidate.modelRef) {
|
|
@@ -1088,9 +1100,8 @@ function resolveSummaryCandidates(params: {
|
|
|
1088
1100
|
});
|
|
1089
1101
|
}
|
|
1090
1102
|
} catch (err) {
|
|
1091
|
-
|
|
1092
|
-
`[lcm] createLcmSummarize: resolveModel FAILED at ${candidate.levelName}
|
|
1093
|
-
err instanceof Error ? err.message : err,
|
|
1103
|
+
params.deps.log.error(
|
|
1104
|
+
`[lcm] createLcmSummarize: resolveModel FAILED at ${candidate.levelName}: ${describeLogError(err)}`,
|
|
1094
1105
|
);
|
|
1095
1106
|
}
|
|
1096
1107
|
}
|
|
@@ -1111,7 +1122,7 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1111
1122
|
}): Promise<{ fn: LcmSummarizeFn; model: string; breakerKey: string } | undefined> {
|
|
1112
1123
|
const resolvedCandidates = resolveSummaryCandidates(params);
|
|
1113
1124
|
if (resolvedCandidates.length === 0) {
|
|
1114
|
-
|
|
1125
|
+
params.deps.log.error("[lcm] createLcmSummarize: no summary model candidates resolved");
|
|
1115
1126
|
return undefined;
|
|
1116
1127
|
}
|
|
1117
1128
|
|
|
@@ -1197,6 +1208,7 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1197
1208
|
requestApiKey: string | undefined,
|
|
1198
1209
|
label: string,
|
|
1199
1210
|
reasoning?: string,
|
|
1211
|
+
options?: { skipModelAuth?: boolean },
|
|
1200
1212
|
) =>
|
|
1201
1213
|
withTimeout(params.deps.complete({
|
|
1202
1214
|
provider,
|
|
@@ -1215,6 +1227,7 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1215
1227
|
],
|
|
1216
1228
|
maxTokens: targetTokens,
|
|
1217
1229
|
...(reasoning ? { reasoning } : {}),
|
|
1230
|
+
...(options?.skipModelAuth === true ? { skipModelAuth: true } : {}),
|
|
1218
1231
|
}), summarizerTimeoutMs, label);
|
|
1219
1232
|
|
|
1220
1233
|
const retryWithoutModelAuth = async (
|
|
@@ -1226,8 +1239,8 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1226
1239
|
if (runtimeManagedAuth) {
|
|
1227
1240
|
throw initialAuthError;
|
|
1228
1241
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1242
|
+
params.deps.log.warn(initialAuthError.message);
|
|
1243
|
+
params.deps.log.warn(
|
|
1231
1244
|
`[lcm] summarizer auth retry: retrying ${provider}/${model} without runtime.modelAuth credentials.`,
|
|
1232
1245
|
);
|
|
1233
1246
|
|
|
@@ -1236,14 +1249,16 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1236
1249
|
skipModelAuth: true,
|
|
1237
1250
|
});
|
|
1238
1251
|
if (!directApiKey) {
|
|
1239
|
-
|
|
1252
|
+
params.deps.log.warn(
|
|
1240
1253
|
`[lcm] summarizer auth retry unavailable: no direct credentials found for ${provider}/${model}.`,
|
|
1241
1254
|
);
|
|
1242
1255
|
throw initialAuthError;
|
|
1243
1256
|
}
|
|
1244
1257
|
|
|
1245
1258
|
try {
|
|
1246
|
-
const directResult = await runSummarizerCall(directApiKey, "auth-retry", reasoning
|
|
1259
|
+
const directResult = await runSummarizerCall(directApiKey, "auth-retry", reasoning, {
|
|
1260
|
+
skipModelAuth: true,
|
|
1261
|
+
});
|
|
1247
1262
|
// Use requireStructuralSignal on the retry success path too — the
|
|
1248
1263
|
// summary text may legitimately contain auth-error phrases.
|
|
1249
1264
|
const directFailure = extractProviderAuthFailure(directResult, {
|
|
@@ -1255,10 +1270,10 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1255
1270
|
model,
|
|
1256
1271
|
failure: directFailure,
|
|
1257
1272
|
});
|
|
1258
|
-
|
|
1273
|
+
params.deps.log.warn(retryAuthError.message);
|
|
1259
1274
|
throw retryAuthError;
|
|
1260
1275
|
}
|
|
1261
|
-
|
|
1276
|
+
params.deps.log.info(
|
|
1262
1277
|
`[lcm] summarizer auth retry succeeded; provider=${provider}; model=${model}; source=direct-credentials`,
|
|
1263
1278
|
);
|
|
1264
1279
|
return directResult;
|
|
@@ -1277,7 +1292,7 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1277
1292
|
model,
|
|
1278
1293
|
failure: directFailure,
|
|
1279
1294
|
});
|
|
1280
|
-
|
|
1295
|
+
params.deps.log.warn(retryAuthError.message);
|
|
1281
1296
|
throw retryAuthError;
|
|
1282
1297
|
}
|
|
1283
1298
|
throw directErr;
|
|
@@ -1317,31 +1332,35 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1317
1332
|
if (err instanceof LcmProviderAuthError) {
|
|
1318
1333
|
lastAuthError = err;
|
|
1319
1334
|
if (nextCandidate) {
|
|
1320
|
-
|
|
1321
|
-
`[lcm]
|
|
1335
|
+
params.deps.log.warn(
|
|
1336
|
+
`[lcm] PROVIDER FALLBACK: ${provider}/${model} auth failed → trying ${nextCandidate.provider}/${nextCandidate.model}`,
|
|
1322
1337
|
);
|
|
1338
|
+
const backoffMs = Math.min(500 * Math.pow(2, index), 8000);
|
|
1339
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1323
1340
|
continue;
|
|
1324
1341
|
}
|
|
1325
1342
|
throw lastAuthError;
|
|
1326
1343
|
}
|
|
1327
1344
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1328
1345
|
const isTimeout = errMsg.includes("summarizer timeout");
|
|
1329
|
-
|
|
1346
|
+
params.deps.log.warn(
|
|
1330
1347
|
`[lcm] summarizer ${isTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${summarizerTimeoutMs}ms; error=${errMsg}`,
|
|
1331
1348
|
);
|
|
1332
1349
|
if (nextCandidate) {
|
|
1333
|
-
|
|
1334
|
-
`[lcm]
|
|
1350
|
+
params.deps.log.warn(
|
|
1351
|
+
`[lcm] PROVIDER FALLBACK: ${provider}/${model} ${isTimeout ? "timed out" : "failed"} → trying ${nextCandidate.provider}/${nextCandidate.model}`,
|
|
1335
1352
|
);
|
|
1353
|
+
const backoffMs = Math.min(500 * Math.pow(2, index), 8000);
|
|
1354
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1336
1355
|
continue;
|
|
1337
1356
|
}
|
|
1338
1357
|
if (err instanceof SummarizerTimeoutError) {
|
|
1339
|
-
|
|
1358
|
+
params.deps.log.warn(
|
|
1340
1359
|
`[lcm] summarizer timed out; provider=${provider}; model=${model}; source=fallback`,
|
|
1341
1360
|
);
|
|
1342
1361
|
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
1343
1362
|
}
|
|
1344
|
-
|
|
1363
|
+
break;
|
|
1345
1364
|
}
|
|
1346
1365
|
|
|
1347
1366
|
const normalized = normalizeCompletionSummary(result.content);
|
|
@@ -1358,7 +1377,7 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1358
1377
|
if (envelopeNormalized.summary) {
|
|
1359
1378
|
summary = envelopeNormalized.summary;
|
|
1360
1379
|
summarySource = "envelope";
|
|
1361
|
-
|
|
1380
|
+
params.deps.log.info(
|
|
1362
1381
|
`[lcm] recovered summary from response envelope; provider=${provider}; model=${model}; ` +
|
|
1363
1382
|
`block_types=${formatBlockTypes(envelopeNormalized.blockTypes)}; source=envelope`,
|
|
1364
1383
|
);
|
|
@@ -1386,7 +1405,7 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1386
1405
|
if (responseDiag) {
|
|
1387
1406
|
diagParts.push(responseDiag);
|
|
1388
1407
|
}
|
|
1389
|
-
|
|
1408
|
+
params.deps.log.warn(`${diagParts.join("; ")}; retrying with conservative settings`);
|
|
1390
1409
|
|
|
1391
1410
|
// Single retry with conservative parameters: low temperature and low
|
|
1392
1411
|
// reasoning budget to coax a textual response from providers that
|
|
@@ -1401,7 +1420,7 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1401
1420
|
|
|
1402
1421
|
if (summary) {
|
|
1403
1422
|
summarySource = "retry";
|
|
1404
|
-
|
|
1423
|
+
params.deps.log.info(
|
|
1405
1424
|
`[lcm] retry succeeded; provider=${provider}; model=${model}; ` +
|
|
1406
1425
|
`block_types=${formatBlockTypes(retryEnvelopeNormalized.blockTypes)}; source=retry`,
|
|
1407
1426
|
);
|
|
@@ -1418,21 +1437,23 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1418
1437
|
retryParts.push(retryDiag);
|
|
1419
1438
|
}
|
|
1420
1439
|
if (nextCandidate) {
|
|
1421
|
-
|
|
1440
|
+
params.deps.log.warn(
|
|
1422
1441
|
`${retryParts.join("; ")}; retrying with ${nextCandidate.provider}/${nextCandidate.model}`,
|
|
1423
1442
|
);
|
|
1424
1443
|
continue;
|
|
1425
1444
|
}
|
|
1426
|
-
|
|
1445
|
+
params.deps.log.warn(`${retryParts.join("; ")}; falling back to truncation`);
|
|
1427
1446
|
summary = initialSummary;
|
|
1428
1447
|
}
|
|
1429
1448
|
} catch (retryErr) {
|
|
1430
1449
|
if (retryErr instanceof LcmProviderAuthError) {
|
|
1431
1450
|
lastAuthError = retryErr;
|
|
1432
1451
|
if (nextCandidate) {
|
|
1433
|
-
|
|
1434
|
-
`[lcm]
|
|
1452
|
+
params.deps.log.warn(
|
|
1453
|
+
`[lcm] PROVIDER FALLBACK: ${provider}/${model} auth failed on retry → trying ${nextCandidate.provider}/${nextCandidate.model}`,
|
|
1435
1454
|
);
|
|
1455
|
+
const backoffMs = Math.min(500 * Math.pow(2, index), 8000);
|
|
1456
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1436
1457
|
continue;
|
|
1437
1458
|
}
|
|
1438
1459
|
throw lastAuthError;
|
|
@@ -1441,12 +1462,12 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1441
1462
|
const retryErrMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
1442
1463
|
const isRetryTimeout = retryErrMsg.includes("summarizer timeout");
|
|
1443
1464
|
if (nextCandidate) {
|
|
1444
|
-
|
|
1465
|
+
params.deps.log.warn(
|
|
1445
1466
|
`[lcm] retry ${isRetryTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${summarizerTimeoutMs}ms; error=${retryErrMsg}; retrying with ${nextCandidate.provider}/${nextCandidate.model}`,
|
|
1446
1467
|
);
|
|
1447
1468
|
continue;
|
|
1448
1469
|
}
|
|
1449
|
-
|
|
1470
|
+
params.deps.log.warn(
|
|
1450
1471
|
`[lcm] retry ${isRetryTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${summarizerTimeoutMs}ms; error=${retryErrMsg}; falling back to truncation`,
|
|
1451
1472
|
);
|
|
1452
1473
|
summary = initialSummary;
|
|
@@ -1455,14 +1476,14 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1455
1476
|
|
|
1456
1477
|
if (!summary) {
|
|
1457
1478
|
summarySource = "fallback";
|
|
1458
|
-
|
|
1479
|
+
params.deps.log.error(
|
|
1459
1480
|
`[lcm] all extraction attempts exhausted; provider=${provider}; model=${model}; source=fallback`,
|
|
1460
1481
|
);
|
|
1461
1482
|
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
1462
1483
|
}
|
|
1463
1484
|
|
|
1464
1485
|
if (summarySource !== "content") {
|
|
1465
|
-
|
|
1486
|
+
params.deps.log.info(
|
|
1466
1487
|
`[lcm] summary resolved via non-content path; provider=${provider}; model=${model}; source=${summarySource}`,
|
|
1467
1488
|
);
|
|
1468
1489
|
}
|
|
@@ -1470,10 +1491,13 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1470
1491
|
return summary;
|
|
1471
1492
|
}
|
|
1472
1493
|
|
|
1494
|
+
params.deps.log.error(
|
|
1495
|
+
`[lcm] ALL PROVIDERS EXHAUSTED: ${resolvedCandidates.length} candidate(s) tried, none succeeded. Compaction falling back to deterministic truncation. Check provider keys and quotas.`,
|
|
1496
|
+
);
|
|
1473
1497
|
if (lastAuthError) {
|
|
1474
1498
|
throw lastAuthError;
|
|
1475
1499
|
}
|
|
1476
|
-
return
|
|
1500
|
+
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
1477
1501
|
};
|
|
1478
1502
|
|
|
1479
1503
|
return {
|
|
@@ -57,7 +57,8 @@ function normalizeRequestedTokenCap(value: unknown): number | undefined {
|
|
|
57
57
|
|
|
58
58
|
export function createLcmDescribeTool(input: {
|
|
59
59
|
deps: LcmDependencies;
|
|
60
|
-
lcm
|
|
60
|
+
lcm?: LcmContextEngine;
|
|
61
|
+
getLcm?: () => Promise<LcmContextEngine>;
|
|
61
62
|
sessionId?: string;
|
|
62
63
|
sessionKey?: string;
|
|
63
64
|
}): AnyAgentTool {
|
|
@@ -71,12 +72,16 @@ export function createLcmDescribeTool(input: {
|
|
|
71
72
|
"token counts, and file exploration results.",
|
|
72
73
|
parameters: LcmDescribeSchema,
|
|
73
74
|
async execute(_toolCallId, params) {
|
|
74
|
-
const
|
|
75
|
-
|
|
75
|
+
const lcm = input.lcm ?? (await input.getLcm?.());
|
|
76
|
+
if (!lcm) {
|
|
77
|
+
throw new Error("LCM engine is unavailable.");
|
|
78
|
+
}
|
|
79
|
+
const retrieval = lcm.getRetrieval();
|
|
80
|
+
const timezone = lcm.timezone;
|
|
76
81
|
const p = params as Record<string, unknown>;
|
|
77
82
|
const id = (p.id as string).trim();
|
|
78
83
|
const conversationScope = await resolveLcmConversationScope({
|
|
79
|
-
lcm
|
|
84
|
+
lcm,
|
|
80
85
|
deps: input.deps,
|
|
81
86
|
sessionId: input.sessionId,
|
|
82
87
|
sessionKey: input.sessionKey,
|
|
@@ -195,7 +195,7 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
195
195
|
"",
|
|
196
196
|
"Strategy:",
|
|
197
197
|
"1. Start with `lcm_describe` on seed summaries to inspect subtree manifests and branch costs.",
|
|
198
|
-
"2. If additional candidates are needed, use `lcm_grep` scoped to summaries.",
|
|
198
|
+
"2. If additional candidates are needed, use `lcm_grep` scoped to summaries. Prefer `mode: \"full_text\"`, quote exact multi-word phrases, use `sort: \"relevance\"` for older-topic recall, and `sort: \"hybrid\"` when recency should still matter.",
|
|
199
199
|
"3. Select branches that fit remaining budget; prefer high-signal paths first.",
|
|
200
200
|
"4. Call `lcm_expand` selectively (do not expand everything blindly).",
|
|
201
201
|
"5. Keep includeMessages=false by default; use includeMessages=true for the message-backed seed summaries above and any other specific leaf evidence.",
|
|
@@ -444,7 +444,8 @@ async function resolveSummaryCandidates(params: {
|
|
|
444
444
|
|
|
445
445
|
export function createLcmExpandQueryTool(input: {
|
|
446
446
|
deps: LcmDependencies;
|
|
447
|
-
lcm
|
|
447
|
+
lcm?: LcmContextEngine;
|
|
448
|
+
getLcm?: () => Promise<LcmContextEngine>;
|
|
448
449
|
/** Session id used for LCM conversation scoping. */
|
|
449
450
|
sessionId?: string;
|
|
450
451
|
/** Requester agent session key used for delegated child session/auth scoping. */
|
|
@@ -462,9 +463,13 @@ export function createLcmExpandQueryTool(input: {
|
|
|
462
463
|
description:
|
|
463
464
|
"Answer a focused question using delegated LCM expansion. " +
|
|
464
465
|
"Find candidate summaries (by IDs or query), expand them in a delegated sub-agent, " +
|
|
465
|
-
"and return a compact prompt-focused answer
|
|
466
|
+
"and return a compact prompt-focused answer. Tool output includes cited summary IDs for follow-up.",
|
|
466
467
|
parameters: LcmExpandQuerySchema,
|
|
467
468
|
async execute(_toolCallId, params) {
|
|
469
|
+
const lcm = input.lcm ?? (await input.getLcm?.());
|
|
470
|
+
if (!lcm) {
|
|
471
|
+
throw new Error("LCM engine is unavailable.");
|
|
472
|
+
}
|
|
468
473
|
const p = params as Record<string, unknown>;
|
|
469
474
|
const explicitSummaryIds = normalizeSummaryIds(p.summaryIds as string[] | undefined);
|
|
470
475
|
const query = typeof p.query === "string" ? p.query.trim() : "";
|
|
@@ -537,7 +542,7 @@ export function createLcmExpandQueryTool(input: {
|
|
|
537
542
|
|
|
538
543
|
try {
|
|
539
544
|
const conversationScope = await resolveLcmConversationScope({
|
|
540
|
-
lcm
|
|
545
|
+
lcm,
|
|
541
546
|
deps: input.deps,
|
|
542
547
|
sessionId: input.sessionId,
|
|
543
548
|
sessionKey: input.sessionKey,
|
|
@@ -552,7 +557,7 @@ export function createLcmExpandQueryTool(input: {
|
|
|
552
557
|
scopedConversationId = await resolveRequesterConversationScopeId({
|
|
553
558
|
deps: input.deps,
|
|
554
559
|
requesterSessionKey: callerSessionKey,
|
|
555
|
-
lcm
|
|
560
|
+
lcm,
|
|
556
561
|
});
|
|
557
562
|
}
|
|
558
563
|
|
|
@@ -564,7 +569,7 @@ export function createLcmExpandQueryTool(input: {
|
|
|
564
569
|
}
|
|
565
570
|
|
|
566
571
|
const candidates = await resolveSummaryCandidates({
|
|
567
|
-
lcm
|
|
572
|
+
lcm,
|
|
568
573
|
explicitSummaryIds,
|
|
569
574
|
query: query || undefined,
|
|
570
575
|
conversationId: scopedConversationId,
|
|
@@ -122,7 +122,8 @@ function buildOrchestrationObservability(input: {
|
|
|
122
122
|
*/
|
|
123
123
|
export function createLcmExpandTool(input: {
|
|
124
124
|
deps: LcmDependencies;
|
|
125
|
-
lcm
|
|
125
|
+
lcm?: LcmContextEngine;
|
|
126
|
+
getLcm?: () => Promise<LcmContextEngine>;
|
|
126
127
|
/** Runtime session key (used for delegated expansion auth scoping). */
|
|
127
128
|
sessionId?: string;
|
|
128
129
|
sessionKey?: string;
|
|
@@ -136,10 +137,14 @@ export function createLcmExpandTool(input: {
|
|
|
136
137
|
"Use this to drill into previously-compacted context when you need detail " +
|
|
137
138
|
"that was summarised away. Provide either summaryIds (direct expansion) or " +
|
|
138
139
|
"query (grep-first, then expand top matches). Returns a compact text payload " +
|
|
139
|
-
"
|
|
140
|
+
"plus cited IDs in tool output for follow-up.",
|
|
140
141
|
parameters: LcmExpandSchema,
|
|
141
142
|
async execute(_toolCallId, params) {
|
|
142
|
-
const
|
|
143
|
+
const lcm = input.lcm ?? (await input.getLcm?.());
|
|
144
|
+
if (!lcm) {
|
|
145
|
+
throw new Error("LCM engine is unavailable.");
|
|
146
|
+
}
|
|
147
|
+
const retrieval = lcm.getRetrieval();
|
|
143
148
|
const orchestrator = new ExpansionOrchestrator(retrieval);
|
|
144
149
|
const runtimeAuthManager = getRuntimeExpansionAuthManager();
|
|
145
150
|
|
|
@@ -178,7 +183,7 @@ export function createLcmExpandTool(input: {
|
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
const conversationScope = await resolveLcmConversationScope({
|
|
181
|
-
lcm
|
|
186
|
+
lcm,
|
|
182
187
|
deps: input.deps,
|
|
183
188
|
sessionId: input.sessionId,
|
|
184
189
|
sessionKey: input.sessionKey,
|