@martian-engineering/lossless-claw 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -11
- package/docs/configuration.md +44 -0
- package/openclaw.plugin.json +114 -0
- package/package.json +2 -1
- package/skills/lossless-claw/SKILL.md +33 -0
- package/skills/lossless-claw/references/architecture.md +52 -0
- package/skills/lossless-claw/references/config.md +263 -0
- package/skills/lossless-claw/references/diagnostics.md +79 -0
- package/skills/lossless-claw/references/recall-tools.md +55 -0
- package/skills/lossless-claw/references/session-lifecycle.md +59 -0
- package/src/assembler.ts +321 -34
- package/src/compaction.ts +220 -19
- package/src/db/config.ts +74 -21
- package/src/db/migration.ts +50 -13
- package/src/engine.ts +742 -133
- package/src/plugin/index.ts +156 -73
- package/src/plugin/lcm-command.ts +759 -0
- package/src/plugin/lcm-doctor-apply.ts +546 -0
- package/src/plugin/lcm-doctor-shared.ts +210 -0
- package/src/store/conversation-store.ts +60 -21
- package/src/store/parse-utc-timestamp.ts +25 -0
- package/src/store/summary-store.ts +460 -11
- package/src/summarize.ts +553 -224
- package/src/tools/lcm-expand-query-tool.ts +195 -59
- package/src/tools/lcm-expansion-recursion-guard.ts +87 -0
- package/src/types.ts +1 -0
package/src/summarize.ts
CHANGED
|
@@ -20,8 +20,32 @@ export type LcmSummarizerLegacyParams = {
|
|
|
20
20
|
authProfileId?: unknown;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
type SummaryResolutionCandidate = {
|
|
24
|
+
levelName: string;
|
|
25
|
+
modelRef: string;
|
|
26
|
+
providerHint?: string;
|
|
27
|
+
hasExplicitProvider: boolean;
|
|
28
|
+
useLegacyAuthProfile: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type ResolvedSummaryCandidate = SummaryResolutionCandidate & {
|
|
32
|
+
provider: string;
|
|
33
|
+
model: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function buildSummarizerBreakerKey(params: {
|
|
37
|
+
candidate: ResolvedSummaryCandidate;
|
|
38
|
+
legacyAuthProfileId?: string;
|
|
39
|
+
}): string {
|
|
40
|
+
const authProfileId = params.candidate.useLegacyAuthProfile
|
|
41
|
+
? (params.legacyAuthProfileId ?? "-")
|
|
42
|
+
: "-";
|
|
43
|
+
return `provider:${params.candidate.provider};model:${params.candidate.model};authProfile:${authProfileId}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
type SummaryMode = "normal" | "aggressive";
|
|
24
47
|
|
|
48
|
+
const DEFAULT_LEAF_TARGET_TOKENS = 2400;
|
|
25
49
|
const DEFAULT_CONDENSED_TARGET_TOKENS = 2000;
|
|
26
50
|
const LCM_SUMMARIZER_SYSTEM_PROMPT =
|
|
27
51
|
"You are a context-compaction summarization engine. Follow user instructions exactly and return plain text summary content only.";
|
|
@@ -35,6 +59,18 @@ const AUTH_ERROR_TEXT_PATTERN =
|
|
|
35
59
|
/\b401\b|unauthorized|unauthorised|invalid[_ -]?token|invalid[_ -]?api[_ -]?key|authentication failed|authorization failed|missing scope|insufficient scope|model\.request\b/i;
|
|
36
60
|
const AUTH_ERROR_STATUS_KEYS = ["status", "statusCode", "status_code"] as const;
|
|
37
61
|
const AUTH_ERROR_NESTED_KEYS = ["error", "response", "cause", "details", "data", "body"] as const;
|
|
62
|
+
const AUTH_ERROR_TOP_LEVEL_KEYS = [
|
|
63
|
+
"error",
|
|
64
|
+
"errorMessage",
|
|
65
|
+
"status",
|
|
66
|
+
"statusCode",
|
|
67
|
+
"status_code",
|
|
68
|
+
"code",
|
|
69
|
+
"details",
|
|
70
|
+
"cause",
|
|
71
|
+
"data",
|
|
72
|
+
"body",
|
|
73
|
+
] as const;
|
|
38
74
|
|
|
39
75
|
type ProviderAuthFailure = {
|
|
40
76
|
statusCode?: number;
|
|
@@ -188,6 +224,15 @@ function collectBlockTypes(value: unknown, out: Set<string>): void {
|
|
|
188
224
|
}
|
|
189
225
|
}
|
|
190
226
|
|
|
227
|
+
/** Treat provider reasoning/thinking payloads as diagnostics, not summary text. */
|
|
228
|
+
function isReasoningLikeType(type: unknown): boolean {
|
|
229
|
+
if (typeof type !== "string") {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
const normalized = type.trim().toLowerCase();
|
|
233
|
+
return normalized.includes("reasoning") || normalized.includes("thinking");
|
|
234
|
+
}
|
|
235
|
+
|
|
191
236
|
/** Collect text payloads from common provider response shapes. */
|
|
192
237
|
function collectTextLikeFields(value: unknown, out: string[]): void {
|
|
193
238
|
if (Array.isArray(value)) {
|
|
@@ -200,7 +245,11 @@ function collectTextLikeFields(value: unknown, out: string[]): void {
|
|
|
200
245
|
return;
|
|
201
246
|
}
|
|
202
247
|
|
|
203
|
-
|
|
248
|
+
if (isReasoningLikeType(value.type)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const key of ["text", "output_text"]) {
|
|
204
253
|
appendTextValue(value[key], out);
|
|
205
254
|
}
|
|
206
255
|
for (const key of ["content", "summary", "output", "message", "response"]) {
|
|
@@ -384,6 +433,21 @@ function extractAuthFailureStatusCode(value: unknown, depth = 0): number | undef
|
|
|
384
433
|
return undefined;
|
|
385
434
|
}
|
|
386
435
|
|
|
436
|
+
function hasTopLevelAuthInspectionKeys(value: Record<string, unknown>): boolean {
|
|
437
|
+
return AUTH_ERROR_TOP_LEVEL_KEYS.some((key) => key in value);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function looksLikeThrownError(value: Record<string, unknown>): boolean {
|
|
441
|
+
return (
|
|
442
|
+
(typeof value.name === "string" && /\berror\b/i.test(value.name)) ||
|
|
443
|
+
"stack" in value ||
|
|
444
|
+
(typeof value.message === "string" &&
|
|
445
|
+
!("content" in value) &&
|
|
446
|
+
!("response" in value) &&
|
|
447
|
+
!("output" in value))
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
387
451
|
function pickAuthInspectionValue(value: unknown): unknown {
|
|
388
452
|
if (!isRecord(value)) {
|
|
389
453
|
return value;
|
|
@@ -393,26 +457,43 @@ function pickAuthInspectionValue(value: unknown): unknown {
|
|
|
393
457
|
}
|
|
394
458
|
|
|
395
459
|
const subset: Record<string, unknown> = {};
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
"status",
|
|
401
|
-
"statusCode",
|
|
402
|
-
"status_code",
|
|
403
|
-
"code",
|
|
404
|
-
"details",
|
|
405
|
-
"response",
|
|
406
|
-
"cause",
|
|
407
|
-
]) {
|
|
460
|
+
const hasTopLevelAuthKeys = hasTopLevelAuthInspectionKeys(value);
|
|
461
|
+
const errorLike = value instanceof Error || looksLikeThrownError(value);
|
|
462
|
+
|
|
463
|
+
for (const key of AUTH_ERROR_TOP_LEVEL_KEYS) {
|
|
408
464
|
if (key in value) {
|
|
409
465
|
subset[key] = value[key];
|
|
410
466
|
}
|
|
411
467
|
}
|
|
412
|
-
|
|
468
|
+
|
|
469
|
+
// Only inspect top-level message payloads when the envelope already looks
|
|
470
|
+
// error-shaped. Successful summary responses also use `message`.
|
|
471
|
+
if ((hasTopLevelAuthKeys || errorLike) && "message" in value) {
|
|
472
|
+
subset.message = value.message;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// `response` can carry either an error payload or successful summary text.
|
|
476
|
+
// Include it only when the surrounding or nested shape already looks like an
|
|
477
|
+
// error envelope.
|
|
478
|
+
if ("response" in value) {
|
|
479
|
+
const response = value.response;
|
|
480
|
+
if (
|
|
481
|
+
hasTopLevelAuthKeys ||
|
|
482
|
+
(isRecord(response) && hasTopLevelAuthInspectionKeys(response)) ||
|
|
483
|
+
(isRecord(response) && looksLikeThrownError(response))
|
|
484
|
+
) {
|
|
485
|
+
subset.response = response;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return Object.keys(subset).length > 0 ? subset : {};
|
|
413
490
|
}
|
|
414
491
|
|
|
415
|
-
|
|
492
|
+
/** @internal Exported for testing only. */
|
|
493
|
+
export function extractProviderAuthFailure(
|
|
494
|
+
value: unknown,
|
|
495
|
+
opts?: { requireStructuralSignal?: boolean },
|
|
496
|
+
): ProviderAuthFailure | undefined {
|
|
416
497
|
const inspectValue = pickAuthInspectionValue(value);
|
|
417
498
|
const statusCode = extractAuthFailureStatusCode(inspectValue);
|
|
418
499
|
const textParts: string[] = [];
|
|
@@ -422,7 +503,20 @@ function extractProviderAuthFailure(value: unknown): ProviderAuthFailure | undef
|
|
|
422
503
|
const hasScopeSignal =
|
|
423
504
|
missingModelRequestScope || /\b(missing|insufficient)\s+scope\b/i.test(normalizedMessage);
|
|
424
505
|
|
|
425
|
-
|
|
506
|
+
// When requireStructuralSignal is set (e.g. checking a successful API response
|
|
507
|
+
// rather than a caught error), only detect auth failures that have a concrete
|
|
508
|
+
// structural indicator (HTTP 401 status code or an explicit provider_auth error
|
|
509
|
+
// kind). Plain text matches in the response body are NOT sufficient — the LLM
|
|
510
|
+
// summary content may legitimately discuss auth errors without being one.
|
|
511
|
+
const hasExplicitErrorKind =
|
|
512
|
+
isRecord(value) && isRecord((value as Record<string, unknown>).error) &&
|
|
513
|
+
((value as Record<string, unknown>).error as Record<string, unknown>).kind === "provider_auth";
|
|
514
|
+
|
|
515
|
+
if (opts?.requireStructuralSignal) {
|
|
516
|
+
if (statusCode !== 401 && !hasExplicitErrorKind) {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
} else if (statusCode !== 401 && !hasScopeSignal && !AUTH_ERROR_TEXT_PATTERN.test(normalizedMessage)) {
|
|
426
520
|
return undefined;
|
|
427
521
|
}
|
|
428
522
|
|
|
@@ -517,6 +611,15 @@ function extractResponseDiagnostics(result: unknown): string {
|
|
|
517
611
|
if (typeof result.provider === "string" && result.provider.trim()) {
|
|
518
612
|
parts.push(`resp_provider=${result.provider.trim()}`);
|
|
519
613
|
}
|
|
614
|
+
if (typeof result.status === "string" && result.status.trim()) {
|
|
615
|
+
parts.push(`status=${result.status.trim()}`);
|
|
616
|
+
}
|
|
617
|
+
if (isRecord(result.incomplete_details) && typeof result.incomplete_details.reason === "string") {
|
|
618
|
+
const reason = result.incomplete_details.reason.trim();
|
|
619
|
+
if (reason) {
|
|
620
|
+
parts.push(`incomplete_reason=${reason}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
520
623
|
for (const key of [
|
|
521
624
|
"request_provider",
|
|
522
625
|
"request_model",
|
|
@@ -580,6 +683,50 @@ function extractResponseDiagnostics(result: unknown): string {
|
|
|
580
683
|
return parts.join("; ");
|
|
581
684
|
}
|
|
582
685
|
|
|
686
|
+
/** Collect retry-worthy "incomplete" signals from Responses-style envelopes/items. */
|
|
687
|
+
function collectIncompleteResponseSignals(
|
|
688
|
+
value: unknown,
|
|
689
|
+
out: Set<string>,
|
|
690
|
+
label = "response",
|
|
691
|
+
depth = 0,
|
|
692
|
+
): void {
|
|
693
|
+
if (depth >= DIAGNOSTIC_MAX_DEPTH) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (Array.isArray(value)) {
|
|
697
|
+
value.slice(0, DIAGNOSTIC_MAX_ARRAY_ITEMS).forEach((entry, index) => {
|
|
698
|
+
collectIncompleteResponseSignals(entry, out, `${label}[${index}]`, depth + 1);
|
|
699
|
+
});
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!isRecord(value)) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (typeof value.status === "string" && value.status.trim().toLowerCase() === "incomplete") {
|
|
707
|
+
out.add(`${label}.status=incomplete`);
|
|
708
|
+
}
|
|
709
|
+
if (isRecord(value.incomplete_details) && typeof value.incomplete_details.reason === "string") {
|
|
710
|
+
const reason = value.incomplete_details.reason.trim();
|
|
711
|
+
if (reason) {
|
|
712
|
+
out.add(`${label}.reason=${reason}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
for (const key of ["content", "output", "message", "response", "items"] as const) {
|
|
717
|
+
if (key in value) {
|
|
718
|
+
collectIncompleteResponseSignals(value[key], out, `${label}.${key}`, depth + 1);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Extract retry-worthy incomplete-response diagnostics for provider envelopes/items. */
|
|
724
|
+
function extractIncompleteResponseSignals(value: unknown): string[] {
|
|
725
|
+
const signals = new Set<string>();
|
|
726
|
+
collectIncompleteResponseSignals(value, signals);
|
|
727
|
+
return [...signals].sort((a, b) => a.localeCompare(b));
|
|
728
|
+
}
|
|
729
|
+
|
|
583
730
|
/**
|
|
584
731
|
* Resolve a practical target token count for leaf and condensed summaries.
|
|
585
732
|
* Aggressive leaf mode intentionally aims lower so compaction converges faster.
|
|
@@ -588,6 +735,7 @@ function resolveTargetTokens(params: {
|
|
|
588
735
|
inputTokens: number;
|
|
589
736
|
mode: SummaryMode;
|
|
590
737
|
isCondensed: boolean;
|
|
738
|
+
leafTargetTokens: number;
|
|
591
739
|
condensedTargetTokens: number;
|
|
592
740
|
}): number {
|
|
593
741
|
if (params.isCondensed) {
|
|
@@ -595,10 +743,12 @@ function resolveTargetTokens(params: {
|
|
|
595
743
|
}
|
|
596
744
|
|
|
597
745
|
const { inputTokens, mode } = params;
|
|
746
|
+
const leafTargetTokens = Math.max(192, params.leafTargetTokens);
|
|
598
747
|
if (mode === "aggressive") {
|
|
599
|
-
|
|
748
|
+
const aggressiveCap = Math.max(96, Math.min(leafTargetTokens, Math.floor(leafTargetTokens * 0.55)));
|
|
749
|
+
return Math.max(96, Math.min(aggressiveCap, Math.floor(inputTokens * 0.2)));
|
|
600
750
|
}
|
|
601
|
-
return Math.max(192, Math.min(
|
|
751
|
+
return Math.max(192, Math.min(leafTargetTokens, Math.floor(inputTokens * 0.35)));
|
|
602
752
|
}
|
|
603
753
|
|
|
604
754
|
/**
|
|
@@ -815,30 +965,47 @@ function buildDeterministicFallbackSummary(text: string, targetTokens: number):
|
|
|
815
965
|
return `${trimmed.slice(0, maxChars)}\n[LCM fallback summary; truncated for context management]`;
|
|
816
966
|
}
|
|
817
967
|
|
|
818
|
-
/**
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
968
|
+
/** Normalize model refs from string or `{ primary }` config shapes. */
|
|
969
|
+
function readModelRef(value: unknown): string {
|
|
970
|
+
if (typeof value === "string") {
|
|
971
|
+
return value.trim();
|
|
972
|
+
}
|
|
973
|
+
const primary = (value as { primary?: unknown } | undefined)?.primary;
|
|
974
|
+
return typeof primary === "string" ? primary.trim() : "";
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** Avoid retrying the same resolved provider/model pair across fallback levels. */
|
|
978
|
+
function dedupeResolvedCandidates(
|
|
979
|
+
candidates: ResolvedSummaryCandidate[],
|
|
980
|
+
): ResolvedSummaryCandidate[] {
|
|
981
|
+
const seen = new Set<string>();
|
|
982
|
+
const ordered: ResolvedSummaryCandidate[] = [];
|
|
983
|
+
for (const candidate of candidates) {
|
|
984
|
+
const key = `${candidate.provider}\u0000${candidate.model}`;
|
|
985
|
+
if (seen.has(key)) {
|
|
986
|
+
continue;
|
|
832
987
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
988
|
+
seen.add(key);
|
|
989
|
+
ordered.push(candidate);
|
|
990
|
+
}
|
|
991
|
+
return ordered;
|
|
992
|
+
}
|
|
836
993
|
|
|
994
|
+
/** Resolve ordered summarizer candidates from env, plugin config, defaults, and session hints. */
|
|
995
|
+
function resolveSummaryCandidates(params: {
|
|
996
|
+
deps: LcmDependencies;
|
|
997
|
+
legacyParams: LcmSummarizerLegacyParams;
|
|
998
|
+
}): ResolvedSummaryCandidate[] {
|
|
999
|
+
const providerHint =
|
|
1000
|
+
typeof params.legacyParams.provider === "string" ? params.legacyParams.provider.trim() : "";
|
|
1001
|
+
const modelHint =
|
|
1002
|
+
typeof params.legacyParams.model === "string" ? params.legacyParams.model.trim() : "";
|
|
837
1003
|
const runtimeConfig =
|
|
838
1004
|
params.legacyParams.config && typeof params.legacyParams.config === "object"
|
|
839
1005
|
? (params.legacyParams.config as {
|
|
840
1006
|
agents?: {
|
|
841
1007
|
defaults?: {
|
|
1008
|
+
model?: unknown;
|
|
842
1009
|
compaction?: {
|
|
843
1010
|
model?: unknown;
|
|
844
1011
|
};
|
|
@@ -853,91 +1020,121 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
853
1020
|
};
|
|
854
1021
|
})
|
|
855
1022
|
: undefined;
|
|
856
|
-
|
|
857
1023
|
const nestedPluginConfig = runtimeConfig?.plugins?.entries?.["lossless-claw"]?.config;
|
|
858
1024
|
|
|
859
|
-
const
|
|
1025
|
+
const resolutionCandidates: SummaryResolutionCandidate[] = [
|
|
860
1026
|
{
|
|
861
1027
|
levelName: "environment variables",
|
|
862
|
-
|
|
863
|
-
|
|
1028
|
+
modelRef: process.env.LCM_SUMMARY_MODEL?.trim() ?? "",
|
|
1029
|
+
providerHint:
|
|
1030
|
+
process.env.LCM_SUMMARY_PROVIDER?.trim() ||
|
|
1031
|
+
(providerHint || undefined),
|
|
1032
|
+
hasExplicitProvider: Boolean(process.env.LCM_SUMMARY_PROVIDER?.trim()),
|
|
1033
|
+
useLegacyAuthProfile: false,
|
|
864
1034
|
},
|
|
865
1035
|
{
|
|
866
1036
|
levelName: "plugin config (lossless-claw)",
|
|
867
|
-
|
|
868
|
-
|
|
1037
|
+
modelRef: readModelRef(nestedPluginConfig?.summaryModel),
|
|
1038
|
+
providerHint:
|
|
1039
|
+
(typeof nestedPluginConfig?.summaryProvider === "string"
|
|
1040
|
+
? nestedPluginConfig.summaryProvider.trim()
|
|
1041
|
+
: "") || (providerHint || undefined),
|
|
1042
|
+
hasExplicitProvider: Boolean(
|
|
1043
|
+
typeof nestedPluginConfig?.summaryProvider === "string" &&
|
|
1044
|
+
nestedPluginConfig.summaryProvider.trim(),
|
|
1045
|
+
),
|
|
1046
|
+
useLegacyAuthProfile: false,
|
|
869
1047
|
},
|
|
870
1048
|
{
|
|
871
1049
|
levelName: "OpenClaw agents.defaults.compaction.model",
|
|
872
|
-
|
|
873
|
-
|
|
1050
|
+
modelRef: readModelRef(runtimeConfig?.agents?.defaults?.compaction?.model),
|
|
1051
|
+
providerHint: undefined,
|
|
1052
|
+
hasExplicitProvider: false,
|
|
1053
|
+
useLegacyAuthProfile: false,
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
levelName: "OpenClaw agents.defaults.model",
|
|
1057
|
+
modelRef: readModelRef(runtimeConfig?.agents?.defaults?.model),
|
|
1058
|
+
providerHint: undefined,
|
|
1059
|
+
hasExplicitProvider: false,
|
|
1060
|
+
useLegacyAuthProfile: false,
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
levelName: "legacy runtime/session model",
|
|
1064
|
+
modelRef: modelHint,
|
|
1065
|
+
providerHint: providerHint || undefined,
|
|
1066
|
+
hasExplicitProvider: Boolean(providerHint),
|
|
1067
|
+
useLegacyAuthProfile: true,
|
|
874
1068
|
},
|
|
875
1069
|
];
|
|
876
1070
|
|
|
877
|
-
|
|
878
|
-
for (const
|
|
879
|
-
if (!
|
|
880
|
-
|
|
881
|
-
resolvedSummary = { model: level.model, provider: undefined };
|
|
882
|
-
break;
|
|
1071
|
+
const resolvedCandidates: ResolvedSummaryCandidate[] = [];
|
|
1072
|
+
for (const candidate of resolutionCandidates) {
|
|
1073
|
+
if (!candidate.modelRef) {
|
|
1074
|
+
continue;
|
|
883
1075
|
}
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
|
|
1076
|
+
if (!candidate.modelRef.includes("/") && !candidate.hasExplicitProvider) {
|
|
1077
|
+
params.deps.log.warn(
|
|
1078
|
+
`[lcm] summaryModel "${candidate.modelRef}" at "${candidate.levelName}" has no summaryProvider or provider prefix. Will attempt resolution without provider.`,
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
const resolved = params.deps.resolveModel(candidate.modelRef, candidate.providerHint);
|
|
1083
|
+
if (resolved.provider && resolved.model) {
|
|
1084
|
+
resolvedCandidates.push({
|
|
1085
|
+
...candidate,
|
|
1086
|
+
provider: resolved.provider,
|
|
1087
|
+
model: resolved.model,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
console.error(
|
|
1092
|
+
`[lcm] createLcmSummarize: resolveModel FAILED at ${candidate.levelName}:`,
|
|
1093
|
+
err instanceof Error ? err.message : err,
|
|
1094
|
+
);
|
|
887
1095
|
}
|
|
888
|
-
params.deps.log.warn(
|
|
889
|
-
`[lcm] summaryModel "${level.model}" at "${level.levelName}" has no summaryProvider or provider prefix. Will attempt resolution without provider.`
|
|
890
|
-
);
|
|
891
|
-
resolvedSummary = { model: level.model, provider: undefined };
|
|
892
|
-
break;
|
|
893
1096
|
}
|
|
894
1097
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const modelHint =
|
|
898
|
-
typeof params.legacyParams.model === "string" ? params.legacyParams.model.trim() : "";
|
|
899
|
-
const modelRef = resolvedSummary?.model || modelHint || undefined;
|
|
900
|
-
|
|
901
|
-
const resolveProviderHint =
|
|
902
|
-
resolvedSummary !== undefined
|
|
903
|
-
? (
|
|
904
|
-
resolvedSummary.provider ||
|
|
905
|
-
(!resolvedSummary.model.includes("/") ? (providerHint || undefined) : undefined)
|
|
906
|
-
)
|
|
907
|
-
: (providerHint || undefined);
|
|
1098
|
+
return dedupeResolvedCandidates(resolvedCandidates);
|
|
1099
|
+
}
|
|
908
1100
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1101
|
+
/**
|
|
1102
|
+
* Builds a model-backed LCM summarize callback from runtime legacy params.
|
|
1103
|
+
*
|
|
1104
|
+
* Returns `undefined` when model/provider context is unavailable so callers can
|
|
1105
|
+
* choose a fallback summarizer.
|
|
1106
|
+
*/
|
|
1107
|
+
export async function createLcmSummarizeFromLegacyParams(params: {
|
|
1108
|
+
deps: LcmDependencies;
|
|
1109
|
+
legacyParams: LcmSummarizerLegacyParams;
|
|
1110
|
+
customInstructions?: string;
|
|
1111
|
+
}): Promise<{ fn: LcmSummarizeFn; model: string; breakerKey: string } | undefined> {
|
|
1112
|
+
const resolvedCandidates = resolveSummaryCandidates(params);
|
|
1113
|
+
if (resolvedCandidates.length === 0) {
|
|
1114
|
+
console.error("[lcm] createLcmSummarize: no summary model candidates resolved");
|
|
914
1115
|
return undefined;
|
|
915
1116
|
}
|
|
916
1117
|
|
|
917
|
-
const { provider, model } = resolved;
|
|
918
|
-
if (!provider || !model) {
|
|
919
|
-
console.error(`[lcm] createLcmSummarize: empty provider="${provider}" or model="${model}"`);
|
|
920
|
-
return undefined;
|
|
921
|
-
}
|
|
922
1118
|
const legacyAuthProfileId =
|
|
923
1119
|
typeof params.legacyParams.authProfileId === "string" &&
|
|
924
1120
|
params.legacyParams.authProfileId.trim()
|
|
925
1121
|
? params.legacyParams.authProfileId.trim()
|
|
926
1122
|
: undefined;
|
|
927
|
-
// When LCM selects a dedicated summarizer model/provider, do not leak the
|
|
928
|
-
// active session's auth profile into that separate credential lookup.
|
|
929
|
-
const authProfileId = resolvedSummary === undefined ? legacyAuthProfileId : undefined;
|
|
930
1123
|
const agentDir =
|
|
931
1124
|
typeof params.legacyParams.agentDir === "string" && params.legacyParams.agentDir.trim()
|
|
932
1125
|
? params.legacyParams.agentDir.trim()
|
|
933
1126
|
: undefined;
|
|
934
|
-
const providerApi = resolveProviderApiFromLegacyConfig(params.legacyParams.config, provider);
|
|
935
1127
|
|
|
936
1128
|
const condensedTargetTokens =
|
|
937
1129
|
Number.isFinite(params.deps.config.condensedTargetTokens) &&
|
|
938
1130
|
params.deps.config.condensedTargetTokens > 0
|
|
939
1131
|
? params.deps.config.condensedTargetTokens
|
|
940
1132
|
: DEFAULT_CONDENSED_TARGET_TOKENS;
|
|
1133
|
+
const leafTargetTokens =
|
|
1134
|
+
Number.isFinite(params.deps.config.leafTargetTokens) &&
|
|
1135
|
+
params.deps.config.leafTargetTokens > 0
|
|
1136
|
+
? params.deps.config.leafTargetTokens
|
|
1137
|
+
: DEFAULT_LEAF_TARGET_TOKENS;
|
|
941
1138
|
|
|
942
1139
|
const fn: LcmSummarizeFn = async (
|
|
943
1140
|
text: string,
|
|
@@ -950,15 +1147,11 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
950
1147
|
|
|
951
1148
|
const mode: SummaryMode = aggressive ? "aggressive" : "normal";
|
|
952
1149
|
const isCondensed = options?.isCondensed === true;
|
|
953
|
-
const apiKey = await params.deps.getApiKey(provider, model, {
|
|
954
|
-
profileId: authProfileId,
|
|
955
|
-
agentDir,
|
|
956
|
-
runtimeConfig: params.legacyParams.config,
|
|
957
|
-
});
|
|
958
1150
|
const targetTokens = resolveTargetTokens({
|
|
959
1151
|
inputTokens: estimateTokens(text),
|
|
960
1152
|
mode,
|
|
961
1153
|
isCondensed,
|
|
1154
|
+
leafTargetTokens,
|
|
962
1155
|
condensedTargetTokens,
|
|
963
1156
|
});
|
|
964
1157
|
const prompt = isCondensed
|
|
@@ -980,96 +1173,30 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
980
1173
|
customInstructions: params.customInstructions,
|
|
981
1174
|
});
|
|
982
1175
|
|
|
983
|
-
let
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1176
|
+
let lastAuthError: LcmProviderAuthError | undefined;
|
|
1177
|
+
|
|
1178
|
+
for (let index = 0; index < resolvedCandidates.length; index += 1) {
|
|
1179
|
+
const candidate = resolvedCandidates[index]!;
|
|
1180
|
+
const provider = candidate.provider;
|
|
1181
|
+
const model = candidate.model;
|
|
1182
|
+
const nextCandidate = index < resolvedCandidates.length - 1 ? resolvedCandidates[index + 1]! : undefined;
|
|
1183
|
+
const authProfileId = candidate.useLegacyAuthProfile ? legacyAuthProfileId : undefined;
|
|
1184
|
+
const providerApi = resolveProviderApiFromLegacyConfig(params.legacyParams.config, provider);
|
|
1185
|
+
const lookupOptions = {
|
|
1186
|
+
profileId: authProfileId,
|
|
991
1187
|
agentDir,
|
|
992
1188
|
runtimeConfig: params.legacyParams.config,
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}), SUMMARIZER_TIMEOUT_MS, "initial");
|
|
1002
|
-
} catch (err) {
|
|
1003
|
-
const authFailure = extractProviderAuthFailure(err);
|
|
1004
|
-
if (authFailure) {
|
|
1005
|
-
const authError = new LcmProviderAuthError({ provider, model, failure: authFailure });
|
|
1006
|
-
console.warn(authError.message);
|
|
1007
|
-
throw authError;
|
|
1008
|
-
}
|
|
1009
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1010
|
-
const isTimeout = errMsg.includes("summarizer timeout");
|
|
1011
|
-
console.warn(
|
|
1012
|
-
`[lcm] summarizer ${isTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${errMsg}`,
|
|
1013
|
-
);
|
|
1014
|
-
if (err instanceof SummarizerTimeoutError) {
|
|
1015
|
-
console.error(
|
|
1016
|
-
`[lcm] summarizer timed out; provider=${provider}; model=${model}; source=fallback`,
|
|
1017
|
-
);
|
|
1018
|
-
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
1019
|
-
}
|
|
1020
|
-
return "";
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
const authFailure = extractProviderAuthFailure(result);
|
|
1024
|
-
if (authFailure) {
|
|
1025
|
-
const authError = new LcmProviderAuthError({ provider, model, failure: authFailure });
|
|
1026
|
-
console.warn(authError.message);
|
|
1027
|
-
throw authError;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const normalized = normalizeCompletionSummary(result.content);
|
|
1031
|
-
let summary = normalized.summary;
|
|
1032
|
-
let summarySource: "content" | "envelope" | "retry" | "fallback" = "content";
|
|
1033
|
-
|
|
1034
|
-
// --- Empty-summary hardening: envelope → retry → deterministic fallback ---
|
|
1035
|
-
if (!summary) {
|
|
1036
|
-
// Envelope-aware extraction: some providers place summary text in
|
|
1037
|
-
// top-level response fields (output, message, response) rather than
|
|
1038
|
-
// inside the content array. Re-run normalization against the full
|
|
1039
|
-
// response envelope before spending an API call on a retry.
|
|
1040
|
-
const envelopeNormalized = normalizeCompletionSummary(result);
|
|
1041
|
-
if (envelopeNormalized.summary) {
|
|
1042
|
-
summary = envelopeNormalized.summary;
|
|
1043
|
-
summarySource = "envelope";
|
|
1044
|
-
console.error(
|
|
1045
|
-
`[lcm] recovered summary from response envelope; provider=${provider}; model=${model}; ` +
|
|
1046
|
-
`block_types=${formatBlockTypes(envelopeNormalized.blockTypes)}; source=envelope`,
|
|
1047
|
-
);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (!summary) {
|
|
1052
|
-
const responseDiag = extractResponseDiagnostics(result);
|
|
1053
|
-
const diagParts = [
|
|
1054
|
-
`[lcm] empty normalized summary on first attempt`,
|
|
1055
|
-
`provider=${provider}`,
|
|
1056
|
-
`model=${model}`,
|
|
1057
|
-
`block_types=${formatBlockTypes(normalized.blockTypes)}`,
|
|
1058
|
-
`response_blocks=${result.content.length}`,
|
|
1059
|
-
];
|
|
1060
|
-
if (responseDiag) {
|
|
1061
|
-
diagParts.push(responseDiag);
|
|
1062
|
-
}
|
|
1063
|
-
console.error(`${diagParts.join("; ")}; retrying with conservative settings`);
|
|
1064
|
-
|
|
1065
|
-
// Single retry with conservative parameters: low temperature and low
|
|
1066
|
-
// reasoning budget to coax a textual response from providers that
|
|
1067
|
-
// sometimes return reasoning-only or empty blocks on the first pass.
|
|
1068
|
-
try {
|
|
1069
|
-
const retryResult = await withTimeout(params.deps.complete({
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
const runSummarizerCall = async (
|
|
1192
|
+
requestApiKey: string | undefined,
|
|
1193
|
+
label: string,
|
|
1194
|
+
reasoning?: string,
|
|
1195
|
+
) =>
|
|
1196
|
+
withTimeout(params.deps.complete({
|
|
1070
1197
|
provider,
|
|
1071
1198
|
model,
|
|
1072
|
-
apiKey,
|
|
1199
|
+
apiKey: requestApiKey,
|
|
1073
1200
|
providerApi,
|
|
1074
1201
|
authProfileId,
|
|
1075
1202
|
agentDir,
|
|
@@ -1082,68 +1209,270 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1082
1209
|
},
|
|
1083
1210
|
],
|
|
1084
1211
|
maxTokens: targetTokens,
|
|
1085
|
-
reasoning:
|
|
1086
|
-
}), SUMMARIZER_TIMEOUT_MS,
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1212
|
+
...(reasoning ? { reasoning } : {}),
|
|
1213
|
+
}), SUMMARIZER_TIMEOUT_MS, label);
|
|
1214
|
+
|
|
1215
|
+
const retryWithoutModelAuth = async (
|
|
1216
|
+
failure: ProviderAuthFailure,
|
|
1217
|
+
reasoning?: string,
|
|
1218
|
+
): Promise<Awaited<ReturnType<typeof params.deps.complete>>> => {
|
|
1219
|
+
const initialAuthError = new LcmProviderAuthError({ provider, model, failure });
|
|
1220
|
+
console.warn(initialAuthError.message);
|
|
1221
|
+
console.warn(
|
|
1222
|
+
`[lcm] summarizer auth retry: retrying ${provider}/${model} without runtime.modelAuth credentials.`,
|
|
1223
|
+
);
|
|
1092
1224
|
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1225
|
+
const directApiKey = await params.deps.getApiKey(provider, model, {
|
|
1226
|
+
...lookupOptions,
|
|
1227
|
+
skipModelAuth: true,
|
|
1228
|
+
});
|
|
1229
|
+
if (!directApiKey) {
|
|
1230
|
+
console.warn(
|
|
1231
|
+
`[lcm] summarizer auth retry unavailable: no direct credentials found for ${provider}/${model}.`,
|
|
1232
|
+
);
|
|
1233
|
+
throw initialAuthError;
|
|
1234
|
+
}
|
|
1095
1235
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1236
|
+
try {
|
|
1237
|
+
const directResult = await runSummarizerCall(directApiKey, "auth-retry", reasoning);
|
|
1238
|
+
// Use requireStructuralSignal on the retry success path too — the
|
|
1239
|
+
// summary text may legitimately contain auth-error phrases.
|
|
1240
|
+
const directFailure = extractProviderAuthFailure(directResult, {
|
|
1241
|
+
requireStructuralSignal: true,
|
|
1242
|
+
});
|
|
1243
|
+
if (directFailure) {
|
|
1244
|
+
const retryAuthError = new LcmProviderAuthError({
|
|
1245
|
+
provider,
|
|
1246
|
+
model,
|
|
1247
|
+
failure: directFailure,
|
|
1248
|
+
});
|
|
1249
|
+
console.warn(retryAuthError.message);
|
|
1250
|
+
throw retryAuthError;
|
|
1251
|
+
}
|
|
1252
|
+
console.warn(
|
|
1253
|
+
`[lcm] summarizer auth retry succeeded; provider=${provider}; model=${model}; source=direct-credentials`,
|
|
1101
1254
|
);
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1255
|
+
return directResult;
|
|
1256
|
+
} catch (directErr) {
|
|
1257
|
+
if (directErr instanceof LcmProviderAuthError) {
|
|
1258
|
+
throw directErr;
|
|
1259
|
+
}
|
|
1260
|
+
// Catch path: real errors carry structural signals (HTTP 401, error.kind),
|
|
1261
|
+
// so requireStructuralSignal is safe here too.
|
|
1262
|
+
const directFailure = extractProviderAuthFailure(directErr, {
|
|
1263
|
+
requireStructuralSignal: true,
|
|
1264
|
+
});
|
|
1265
|
+
if (directFailure) {
|
|
1266
|
+
const retryAuthError = new LcmProviderAuthError({
|
|
1267
|
+
provider,
|
|
1268
|
+
model,
|
|
1269
|
+
failure: directFailure,
|
|
1270
|
+
});
|
|
1271
|
+
console.warn(retryAuthError.message);
|
|
1272
|
+
throw retryAuthError;
|
|
1273
|
+
}
|
|
1274
|
+
throw directErr;
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const attemptSummarizerCall = async (
|
|
1279
|
+
label: string,
|
|
1280
|
+
reasoning?: string,
|
|
1281
|
+
): Promise<Awaited<ReturnType<typeof params.deps.complete>>> => {
|
|
1282
|
+
const apiKey = await params.deps.getApiKey(provider, model, lookupOptions);
|
|
1283
|
+
try {
|
|
1284
|
+
const result = await runSummarizerCall(apiKey, label, reasoning);
|
|
1285
|
+
// Use requireStructuralSignal so that LLM summary text containing
|
|
1286
|
+
// auth-related words (e.g. "provider auth error") is NOT mistaken
|
|
1287
|
+
// for an actual API auth failure.
|
|
1288
|
+
const authFailure = extractProviderAuthFailure(result, {
|
|
1289
|
+
requireStructuralSignal: true,
|
|
1290
|
+
});
|
|
1291
|
+
if (!authFailure) {
|
|
1292
|
+
return result;
|
|
1293
|
+
}
|
|
1294
|
+
return retryWithoutModelAuth(authFailure, reasoning);
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
const authFailure = extractProviderAuthFailure(err);
|
|
1297
|
+
if (!authFailure) {
|
|
1298
|
+
throw err;
|
|
1113
1299
|
}
|
|
1114
|
-
|
|
1300
|
+
return retryWithoutModelAuth(authFailure, reasoning);
|
|
1115
1301
|
}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
let result: Awaited<ReturnType<typeof params.deps.complete>>;
|
|
1305
|
+
try {
|
|
1306
|
+
result = await attemptSummarizerCall("initial");
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
if (err instanceof LcmProviderAuthError) {
|
|
1309
|
+
lastAuthError = err;
|
|
1310
|
+
if (nextCandidate) {
|
|
1311
|
+
console.warn(
|
|
1312
|
+
`[lcm] summarizer auth fallback: retrying with ${nextCandidate.provider}/${nextCandidate.model} after ${provider}/${model} failed auth.`,
|
|
1313
|
+
);
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
throw lastAuthError;
|
|
1121
1317
|
}
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
const isRetryTimeout = retryErrMsg.includes("summarizer timeout");
|
|
1318
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1319
|
+
const isTimeout = errMsg.includes("summarizer timeout");
|
|
1125
1320
|
console.warn(
|
|
1126
|
-
`[lcm]
|
|
1321
|
+
`[lcm] summarizer ${isTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${errMsg}`,
|
|
1127
1322
|
);
|
|
1323
|
+
if (nextCandidate) {
|
|
1324
|
+
console.warn(
|
|
1325
|
+
`[lcm] summarizer candidate fallback: retrying with ${nextCandidate.provider}/${nextCandidate.model} after ${provider}/${model} ${isTimeout ? "timed out" : "failed"}.`,
|
|
1326
|
+
);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (err instanceof SummarizerTimeoutError) {
|
|
1330
|
+
console.error(
|
|
1331
|
+
`[lcm] summarizer timed out; provider=${provider}; model=${model}; source=fallback`,
|
|
1332
|
+
);
|
|
1333
|
+
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
1334
|
+
}
|
|
1335
|
+
return "";
|
|
1128
1336
|
}
|
|
1129
|
-
}
|
|
1130
1337
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1338
|
+
const normalized = normalizeCompletionSummary(result.content);
|
|
1339
|
+
let summary = normalized.summary;
|
|
1340
|
+
let summarySource: "content" | "envelope" | "retry" | "fallback" = "content";
|
|
1341
|
+
|
|
1342
|
+
// --- Empty-summary hardening: envelope → retry → deterministic fallback ---
|
|
1343
|
+
if (!summary) {
|
|
1344
|
+
// Envelope-aware extraction: some providers place summary text in
|
|
1345
|
+
// top-level response fields (output, message, response) rather than
|
|
1346
|
+
// inside the content array. Re-run normalization against the full
|
|
1347
|
+
// response envelope before spending an API call on a retry.
|
|
1348
|
+
const envelopeNormalized = normalizeCompletionSummary(result);
|
|
1349
|
+
if (envelopeNormalized.summary) {
|
|
1350
|
+
summary = envelopeNormalized.summary;
|
|
1351
|
+
summarySource = "envelope";
|
|
1352
|
+
console.error(
|
|
1353
|
+
`[lcm] recovered summary from response envelope; provider=${provider}; model=${model}; ` +
|
|
1354
|
+
`block_types=${formatBlockTypes(envelopeNormalized.blockTypes)}; source=envelope`,
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1138
1358
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1359
|
+
const incompleteSignals = extractIncompleteResponseSignals(result);
|
|
1360
|
+
const initialSummary = summary;
|
|
1361
|
+
const shouldRetryIncompleteSummary = summary.length > 0 && incompleteSignals.length > 0;
|
|
1362
|
+
|
|
1363
|
+
if (!summary || shouldRetryIncompleteSummary) {
|
|
1364
|
+
const responseDiag = extractResponseDiagnostics(result);
|
|
1365
|
+
const diagParts = [
|
|
1366
|
+
shouldRetryIncompleteSummary
|
|
1367
|
+
? `[lcm] incomplete summary response on first attempt`
|
|
1368
|
+
: `[lcm] empty normalized summary on first attempt`,
|
|
1369
|
+
`provider=${provider}`,
|
|
1370
|
+
`model=${model}`,
|
|
1371
|
+
`block_types=${formatBlockTypes(normalized.blockTypes)}`,
|
|
1372
|
+
`response_blocks=${result.content.length}`,
|
|
1373
|
+
];
|
|
1374
|
+
if (incompleteSignals.length > 0) {
|
|
1375
|
+
diagParts.push(`incomplete=${incompleteSignals.join(",")}`);
|
|
1376
|
+
}
|
|
1377
|
+
if (responseDiag) {
|
|
1378
|
+
diagParts.push(responseDiag);
|
|
1379
|
+
}
|
|
1380
|
+
console.error(`${diagParts.join("; ")}; retrying with conservative settings`);
|
|
1381
|
+
|
|
1382
|
+
// Single retry with conservative parameters: low temperature and low
|
|
1383
|
+
// reasoning budget to coax a textual response from providers that
|
|
1384
|
+
// sometimes return reasoning-only or empty blocks on the first pass.
|
|
1385
|
+
try {
|
|
1386
|
+
const retryResult = await attemptSummarizerCall("retry", "low");
|
|
1387
|
+
const retryNormalized = normalizeCompletionSummary(retryResult.content);
|
|
1388
|
+
const retryEnvelopeNormalized = retryNormalized.summary
|
|
1389
|
+
? retryNormalized
|
|
1390
|
+
: normalizeCompletionSummary(retryResult);
|
|
1391
|
+
summary = retryEnvelopeNormalized.summary;
|
|
1392
|
+
|
|
1393
|
+
if (summary) {
|
|
1394
|
+
summarySource = "retry";
|
|
1395
|
+
console.error(
|
|
1396
|
+
`[lcm] retry succeeded; provider=${provider}; model=${model}; ` +
|
|
1397
|
+
`block_types=${formatBlockTypes(retryEnvelopeNormalized.blockTypes)}; source=retry`,
|
|
1398
|
+
);
|
|
1399
|
+
} else {
|
|
1400
|
+
const retryDiag = extractResponseDiagnostics(retryResult);
|
|
1401
|
+
const retryParts = [
|
|
1402
|
+
`[lcm] retry also returned empty summary`,
|
|
1403
|
+
`provider=${provider}`,
|
|
1404
|
+
`model=${model}`,
|
|
1405
|
+
`block_types=${formatBlockTypes(retryEnvelopeNormalized.blockTypes)}`,
|
|
1406
|
+
`response_blocks=${retryResult.content.length}`,
|
|
1407
|
+
];
|
|
1408
|
+
if (retryDiag) {
|
|
1409
|
+
retryParts.push(retryDiag);
|
|
1410
|
+
}
|
|
1411
|
+
if (nextCandidate) {
|
|
1412
|
+
console.warn(
|
|
1413
|
+
`${retryParts.join("; ")}; retrying with ${nextCandidate.provider}/${nextCandidate.model}`,
|
|
1414
|
+
);
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
console.error(`${retryParts.join("; ")}; falling back to truncation`);
|
|
1418
|
+
summary = initialSummary;
|
|
1419
|
+
}
|
|
1420
|
+
} catch (retryErr) {
|
|
1421
|
+
if (retryErr instanceof LcmProviderAuthError) {
|
|
1422
|
+
lastAuthError = retryErr;
|
|
1423
|
+
if (nextCandidate) {
|
|
1424
|
+
console.warn(
|
|
1425
|
+
`[lcm] summarizer auth fallback: retrying with ${nextCandidate.provider}/${nextCandidate.model} after ${provider}/${model} failed auth.`,
|
|
1426
|
+
);
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
throw lastAuthError;
|
|
1430
|
+
}
|
|
1431
|
+
// Retry is best-effort; log and proceed to deterministic fallback.
|
|
1432
|
+
const retryErrMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
1433
|
+
const isRetryTimeout = retryErrMsg.includes("summarizer timeout");
|
|
1434
|
+
if (nextCandidate) {
|
|
1435
|
+
console.warn(
|
|
1436
|
+
`[lcm] retry ${isRetryTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${retryErrMsg}; retrying with ${nextCandidate.provider}/${nextCandidate.model}`,
|
|
1437
|
+
);
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
console.warn(
|
|
1441
|
+
`[lcm] retry ${isRetryTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${retryErrMsg}; falling back to truncation`,
|
|
1442
|
+
);
|
|
1443
|
+
summary = initialSummary;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (!summary) {
|
|
1448
|
+
summarySource = "fallback";
|
|
1449
|
+
console.error(
|
|
1450
|
+
`[lcm] all extraction attempts exhausted; provider=${provider}; model=${model}; source=fallback`,
|
|
1451
|
+
);
|
|
1452
|
+
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (summarySource !== "content") {
|
|
1456
|
+
console.error(
|
|
1457
|
+
`[lcm] summary resolved via non-content path; provider=${provider}; model=${model}; source=${summarySource}`,
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
return summary;
|
|
1143
1462
|
}
|
|
1144
1463
|
|
|
1145
|
-
|
|
1464
|
+
if (lastAuthError) {
|
|
1465
|
+
throw lastAuthError;
|
|
1466
|
+
}
|
|
1467
|
+
return "";
|
|
1146
1468
|
};
|
|
1147
1469
|
|
|
1148
|
-
return {
|
|
1470
|
+
return {
|
|
1471
|
+
fn,
|
|
1472
|
+
model: resolvedCandidates[0]!.model,
|
|
1473
|
+
breakerKey: buildSummarizerBreakerKey({
|
|
1474
|
+
candidate: resolvedCandidates[0]!,
|
|
1475
|
+
legacyAuthProfileId,
|
|
1476
|
+
}),
|
|
1477
|
+
};
|
|
1149
1478
|
}
|