@martian-engineering/lossless-claw 0.6.3 → 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 +15 -3
- 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 +814 -97
- package/src/lcm-log.ts +37 -0
- package/src/plugin/index.ts +398 -83
- 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
package/src/engine.ts
CHANGED
|
@@ -43,9 +43,16 @@ import {
|
|
|
43
43
|
generateExplorationSummary,
|
|
44
44
|
parseFileBlocks,
|
|
45
45
|
} from "./large-files.js";
|
|
46
|
+
import { describeLogError } from "./lcm-log.js";
|
|
46
47
|
import { RetrievalEngine } from "./retrieval.js";
|
|
47
48
|
import { compileSessionPatterns, matchesSessionPattern } from "./session-patterns.js";
|
|
48
49
|
import { logStartupBannerOnce } from "./startup-banner-log.js";
|
|
50
|
+
import {
|
|
51
|
+
CompactionTelemetryStore,
|
|
52
|
+
type ConversationCompactionTelemetryRecord,
|
|
53
|
+
type CacheState,
|
|
54
|
+
type ActivityBand,
|
|
55
|
+
} from "./store/compaction-telemetry-store.js";
|
|
49
56
|
import {
|
|
50
57
|
ConversationStore,
|
|
51
58
|
type ConversationRecord,
|
|
@@ -63,6 +70,30 @@ type CircuitBreakerState = {
|
|
|
63
70
|
failures: number;
|
|
64
71
|
openSince: number | null;
|
|
65
72
|
};
|
|
73
|
+
type PromptCacheSnapshot = {
|
|
74
|
+
lastObservedCacheRead?: number;
|
|
75
|
+
lastObservedCacheWrite?: number;
|
|
76
|
+
cacheState: CacheState;
|
|
77
|
+
retention?: string;
|
|
78
|
+
sawExplicitBreak: boolean;
|
|
79
|
+
};
|
|
80
|
+
type IncrementalCompactionDecision = {
|
|
81
|
+
shouldCompact: boolean;
|
|
82
|
+
cacheState: CacheState;
|
|
83
|
+
maxPasses: number;
|
|
84
|
+
rawTokensOutsideTail: number;
|
|
85
|
+
threshold: number;
|
|
86
|
+
leafChunkTokens: number;
|
|
87
|
+
fallbackLeafChunkTokens: number[];
|
|
88
|
+
activityBand: ActivityBand;
|
|
89
|
+
allowCondensedPasses: boolean;
|
|
90
|
+
};
|
|
91
|
+
type DynamicLeafChunkBounds = {
|
|
92
|
+
floor: number;
|
|
93
|
+
medium: number;
|
|
94
|
+
high: number;
|
|
95
|
+
max: number;
|
|
96
|
+
};
|
|
66
97
|
type TranscriptRewriteReplacement = {
|
|
67
98
|
entryId: string;
|
|
68
99
|
message: AgentMessage;
|
|
@@ -83,6 +114,13 @@ type ContextEngineMaintenanceRuntimeContext = Record<string, unknown> & {
|
|
|
83
114
|
};
|
|
84
115
|
|
|
85
116
|
const TRANSCRIPT_GC_BATCH_SIZE = 12;
|
|
117
|
+
const HOT_CACHE_HYSTERESIS_TURNS = 2;
|
|
118
|
+
const DYNAMIC_LEAF_CHUNK_MEDIUM_MULTIPLIER = 1.5;
|
|
119
|
+
const DYNAMIC_LEAF_CHUNK_HIGH_MULTIPLIER = 2;
|
|
120
|
+
const DYNAMIC_ACTIVITY_MEDIUM_UPSHIFT_FACTOR = 0.5;
|
|
121
|
+
const DYNAMIC_ACTIVITY_MEDIUM_DOWNSHIFT_FACTOR = 0.35;
|
|
122
|
+
const DYNAMIC_ACTIVITY_HIGH_UPSHIFT_FACTOR = 1.0;
|
|
123
|
+
const DYNAMIC_ACTIVITY_HIGH_DOWNSHIFT_FACTOR = 0.75;
|
|
86
124
|
|
|
87
125
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
88
126
|
|
|
@@ -835,12 +873,25 @@ function isBootstrapMessage(value: unknown): value is AgentMessage {
|
|
|
835
873
|
return "content" in msg || ("command" in msg && "output" in msg);
|
|
836
874
|
}
|
|
837
875
|
|
|
876
|
+
function extractCanonicalBootstrapMessage(value: unknown): AgentMessage | null {
|
|
877
|
+
if (isBootstrapMessage(value)) {
|
|
878
|
+
return value;
|
|
879
|
+
}
|
|
880
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
const entry = value as { type?: unknown; message?: unknown };
|
|
884
|
+
if ("message" in entry) {
|
|
885
|
+
if (entry.type !== undefined && entry.type !== "message") {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
return isBootstrapMessage(entry.message) ? entry.message : null;
|
|
889
|
+
}
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
|
|
838
893
|
function extractBootstrapMessageCandidate(value: unknown): AgentMessage | null {
|
|
839
|
-
|
|
840
|
-
value && typeof value === "object" && "message" in value
|
|
841
|
-
? (value as { message?: unknown }).message
|
|
842
|
-
: value;
|
|
843
|
-
return isBootstrapMessage(candidate) ? candidate : null;
|
|
894
|
+
return extractCanonicalBootstrapMessage(value);
|
|
844
895
|
}
|
|
845
896
|
|
|
846
897
|
function parseBootstrapJsonl(raw: string, options?: {
|
|
@@ -863,9 +914,6 @@ function parseBootstrapJsonl(raw: string, options?: {
|
|
|
863
914
|
messages.push(candidate);
|
|
864
915
|
continue;
|
|
865
916
|
}
|
|
866
|
-
if (options?.strict) {
|
|
867
|
-
hadMalformedLine = true;
|
|
868
|
-
}
|
|
869
917
|
} catch {
|
|
870
918
|
if (options?.strict) {
|
|
871
919
|
hadMalformedLine = true;
|
|
@@ -1018,7 +1066,12 @@ function readFileSegment(sessionFile: string, offset: number): string | null {
|
|
|
1018
1066
|
}
|
|
1019
1067
|
}
|
|
1020
1068
|
|
|
1021
|
-
function readLastJsonlEntryBeforeOffset(
|
|
1069
|
+
function readLastJsonlEntryBeforeOffset(
|
|
1070
|
+
sessionFile: string,
|
|
1071
|
+
offset: number,
|
|
1072
|
+
messageOnly = false,
|
|
1073
|
+
matcher?: (message: AgentMessage) => boolean,
|
|
1074
|
+
): string | null {
|
|
1022
1075
|
const chunkSize = 16_384;
|
|
1023
1076
|
let fd: number | null = null;
|
|
1024
1077
|
try {
|
|
@@ -1056,11 +1109,11 @@ function readLastJsonlEntryBeforeOffset(sessionFile: string, offset: number, mes
|
|
|
1056
1109
|
const candidate = trimmedEnd.slice(newlineIndex + 1).trim();
|
|
1057
1110
|
if (candidate) {
|
|
1058
1111
|
if (messageOnly) {
|
|
1059
|
-
let
|
|
1112
|
+
let matchedMessage: AgentMessage | null = null;
|
|
1060
1113
|
try {
|
|
1061
|
-
|
|
1114
|
+
matchedMessage = extractBootstrapMessageCandidate(JSON.parse(candidate));
|
|
1062
1115
|
} catch { /* not valid JSON, skip */ }
|
|
1063
|
-
if (!
|
|
1116
|
+
if (!matchedMessage || (matcher && !matcher(matchedMessage))) {
|
|
1064
1117
|
carry = trimmedEnd.slice(0, newlineIndex);
|
|
1065
1118
|
continue;
|
|
1066
1119
|
}
|
|
@@ -1075,11 +1128,11 @@ function readLastJsonlEntryBeforeOffset(sessionFile: string, offset: number, mes
|
|
|
1075
1128
|
if (reachedStart) {
|
|
1076
1129
|
const firstLine = trimmedEnd.trim() || null;
|
|
1077
1130
|
if (firstLine && messageOnly) {
|
|
1078
|
-
let
|
|
1131
|
+
let matchedMessage: AgentMessage | null = null;
|
|
1079
1132
|
try {
|
|
1080
|
-
|
|
1133
|
+
matchedMessage = extractBootstrapMessageCandidate(JSON.parse(firstLine));
|
|
1081
1134
|
} catch { /* not valid JSON */ }
|
|
1082
|
-
if (!
|
|
1135
|
+
if (!matchedMessage || (matcher && !matcher(matchedMessage))) return null;
|
|
1083
1136
|
}
|
|
1084
1137
|
return firstLine;
|
|
1085
1138
|
}
|
|
@@ -1155,6 +1208,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1155
1208
|
|
|
1156
1209
|
private conversationStore: ConversationStore;
|
|
1157
1210
|
private summaryStore: SummaryStore;
|
|
1211
|
+
private compactionTelemetryStore: CompactionTelemetryStore;
|
|
1158
1212
|
private assembler: ContextAssembler;
|
|
1159
1213
|
private compaction: CompactionEngine;
|
|
1160
1214
|
private retrieval: RetrievalEngine;
|
|
@@ -1224,6 +1278,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1224
1278
|
fts5Available: this.fts5Available,
|
|
1225
1279
|
});
|
|
1226
1280
|
this.summaryStore = new SummaryStore(this.db, { fts5Available: this.fts5Available });
|
|
1281
|
+
this.compactionTelemetryStore = new CompactionTelemetryStore(this.db);
|
|
1227
1282
|
|
|
1228
1283
|
if (!this.fts5Available) {
|
|
1229
1284
|
this.deps.log.warn(
|
|
@@ -1269,6 +1324,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1269
1324
|
this.conversationStore,
|
|
1270
1325
|
this.summaryStore,
|
|
1271
1326
|
compactionConfig,
|
|
1327
|
+
this.deps.log,
|
|
1272
1328
|
);
|
|
1273
1329
|
|
|
1274
1330
|
this.retrieval = new RetrievalEngine(this.conversationStore, this.summaryStore);
|
|
@@ -1335,10 +1391,17 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1335
1391
|
private recordCompactionAuthFailure(key: string): void {
|
|
1336
1392
|
const state = this.getCircuitBreakerState(key);
|
|
1337
1393
|
state.failures++;
|
|
1394
|
+
const halfThreshold = Math.ceil(this.config.circuitBreakerThreshold / 2);
|
|
1395
|
+
if (state.failures === halfThreshold && state.failures < this.config.circuitBreakerThreshold) {
|
|
1396
|
+
this.deps.log.warn(
|
|
1397
|
+
`[lcm] WARNING: compaction degraded — ${state.failures}/${this.config.circuitBreakerThreshold} consecutive auth failures for ${key}`,
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1338
1400
|
if (state.failures >= this.config.circuitBreakerThreshold) {
|
|
1339
1401
|
state.openSince = Date.now();
|
|
1340
|
-
|
|
1341
|
-
|
|
1402
|
+
const cooldownMin = Math.round(this.config.circuitBreakerCooldownMs / 60000);
|
|
1403
|
+
this.deps.log.warn(
|
|
1404
|
+
`[lcm] CIRCUIT BREAKER OPEN: compaction disabled for ${key}. Auto-retry in ${cooldownMin}m. LCM is operating in degraded mode.`,
|
|
1342
1405
|
);
|
|
1343
1406
|
}
|
|
1344
1407
|
}
|
|
@@ -1349,7 +1412,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1349
1412
|
return;
|
|
1350
1413
|
}
|
|
1351
1414
|
if (state.failures > 0 || state.openSince !== null) {
|
|
1352
|
-
|
|
1415
|
+
this.deps.log.info(
|
|
1353
1416
|
`[lcm] compaction circuit breaker CLOSED: successful compaction for ${key} after ${state.failures} prior failures.`,
|
|
1354
1417
|
);
|
|
1355
1418
|
}
|
|
@@ -1446,6 +1509,486 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1446
1509
|
return cap != null && cap > 0 ? Math.min(budget, cap) : budget;
|
|
1447
1510
|
}
|
|
1448
1511
|
|
|
1512
|
+
/** Normalize token counters that may legitimately be zero. */
|
|
1513
|
+
private normalizeOptionalCount(value: unknown): number | undefined {
|
|
1514
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
1515
|
+
return undefined;
|
|
1516
|
+
}
|
|
1517
|
+
return Math.floor(value);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/** Treat a recent cache hit as still-hot for a couple of turns unless telemetry observed a later break. */
|
|
1521
|
+
private shouldApplyHotCacheHysteresis(
|
|
1522
|
+
telemetry: ConversationCompactionTelemetryRecord | null,
|
|
1523
|
+
): boolean {
|
|
1524
|
+
if (!telemetry?.lastObservedCacheHitAt) {
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
if (
|
|
1528
|
+
telemetry.lastObservedCacheBreakAt
|
|
1529
|
+
&& telemetry.lastObservedCacheBreakAt >= telemetry.lastObservedCacheHitAt
|
|
1530
|
+
) {
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
return telemetry.turnsSinceLeafCompaction <= HOT_CACHE_HYSTERESIS_TURNS;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/** Resolve the effective cache state the incremental compaction policy should react to. */
|
|
1537
|
+
private resolveCacheAwareState(
|
|
1538
|
+
telemetry: ConversationCompactionTelemetryRecord | null,
|
|
1539
|
+
): CacheState {
|
|
1540
|
+
if (!telemetry) {
|
|
1541
|
+
return "unknown";
|
|
1542
|
+
}
|
|
1543
|
+
if (telemetry.cacheState === "hot") {
|
|
1544
|
+
return "hot";
|
|
1545
|
+
}
|
|
1546
|
+
if (this.shouldApplyHotCacheHysteresis(telemetry)) {
|
|
1547
|
+
return "hot";
|
|
1548
|
+
}
|
|
1549
|
+
return telemetry.cacheState;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/** Decide whether a hot cache still has enough real token-budget headroom to skip incremental maintenance. */
|
|
1553
|
+
private isComfortablyUnderTokenBudget(params: {
|
|
1554
|
+
currentTokenCount?: number;
|
|
1555
|
+
tokenBudget: number;
|
|
1556
|
+
}): boolean {
|
|
1557
|
+
if (
|
|
1558
|
+
typeof params.currentTokenCount !== "number"
|
|
1559
|
+
|| !Number.isFinite(params.currentTokenCount)
|
|
1560
|
+
|| params.currentTokenCount < 0
|
|
1561
|
+
) {
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
const budget = Math.max(1, Math.floor(params.tokenBudget));
|
|
1565
|
+
const safeBudget = Math.floor(
|
|
1566
|
+
budget * (1 - this.config.cacheAwareCompaction.hotCacheBudgetHeadroomRatio),
|
|
1567
|
+
);
|
|
1568
|
+
return params.currentTokenCount <= safeBudget;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/** Resolve bounded dynamic leaf chunk sizes from config and the active token budget. */
|
|
1572
|
+
private resolveDynamicLeafChunkBounds(tokenBudget?: number): DynamicLeafChunkBounds {
|
|
1573
|
+
const floor = Math.max(1, Math.floor(this.config.leafChunkTokens));
|
|
1574
|
+
const configuredMax = this.config.dynamicLeafChunkTokens.enabled
|
|
1575
|
+
? Math.max(floor, Math.floor(this.config.dynamicLeafChunkTokens.max))
|
|
1576
|
+
: floor;
|
|
1577
|
+
const budgetCap =
|
|
1578
|
+
typeof tokenBudget === "number" &&
|
|
1579
|
+
Number.isFinite(tokenBudget) &&
|
|
1580
|
+
tokenBudget > 0
|
|
1581
|
+
? Math.max(floor, Math.floor(tokenBudget * this.config.contextThreshold))
|
|
1582
|
+
: configuredMax;
|
|
1583
|
+
const max = Math.max(floor, Math.min(configuredMax, budgetCap));
|
|
1584
|
+
const medium = Math.max(
|
|
1585
|
+
floor,
|
|
1586
|
+
Math.min(max, Math.floor(floor * DYNAMIC_LEAF_CHUNK_MEDIUM_MULTIPLIER)),
|
|
1587
|
+
);
|
|
1588
|
+
const high = Math.max(
|
|
1589
|
+
floor,
|
|
1590
|
+
Math.min(max, Math.floor(floor * DYNAMIC_LEAF_CHUNK_HIGH_MULTIPLIER)),
|
|
1591
|
+
);
|
|
1592
|
+
return { floor, medium, high, max };
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/** Classify the current refill rate into a simple step band with downshift hysteresis. */
|
|
1596
|
+
private classifyDynamicLeafActivityBand(params: {
|
|
1597
|
+
lastActivityBand?: ActivityBand;
|
|
1598
|
+
tokensAccumulatedSinceLeafCompaction: number;
|
|
1599
|
+
turnsSinceLeafCompaction: number;
|
|
1600
|
+
floor: number;
|
|
1601
|
+
}): ActivityBand {
|
|
1602
|
+
const turns = Math.max(1, params.turnsSinceLeafCompaction);
|
|
1603
|
+
const tokensPerTurn = params.tokensAccumulatedSinceLeafCompaction / turns;
|
|
1604
|
+
const mediumUpshift = params.floor * DYNAMIC_ACTIVITY_MEDIUM_UPSHIFT_FACTOR;
|
|
1605
|
+
const mediumDownshift = params.floor * DYNAMIC_ACTIVITY_MEDIUM_DOWNSHIFT_FACTOR;
|
|
1606
|
+
const highUpshift = params.floor * DYNAMIC_ACTIVITY_HIGH_UPSHIFT_FACTOR;
|
|
1607
|
+
const highDownshift = params.floor * DYNAMIC_ACTIVITY_HIGH_DOWNSHIFT_FACTOR;
|
|
1608
|
+
const lastBand = params.lastActivityBand ?? "low";
|
|
1609
|
+
|
|
1610
|
+
if (lastBand === "high") {
|
|
1611
|
+
if (tokensPerTurn >= highDownshift) {
|
|
1612
|
+
return "high";
|
|
1613
|
+
}
|
|
1614
|
+
return tokensPerTurn >= mediumDownshift ? "medium" : "low";
|
|
1615
|
+
}
|
|
1616
|
+
if (lastBand === "medium") {
|
|
1617
|
+
if (tokensPerTurn >= highUpshift) {
|
|
1618
|
+
return "high";
|
|
1619
|
+
}
|
|
1620
|
+
if (tokensPerTurn < mediumDownshift) {
|
|
1621
|
+
return "low";
|
|
1622
|
+
}
|
|
1623
|
+
return "medium";
|
|
1624
|
+
}
|
|
1625
|
+
if (tokensPerTurn >= highUpshift) {
|
|
1626
|
+
return "high";
|
|
1627
|
+
}
|
|
1628
|
+
if (tokensPerTurn >= mediumUpshift) {
|
|
1629
|
+
return "medium";
|
|
1630
|
+
}
|
|
1631
|
+
return "low";
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/** Map an activity band to the corresponding working leaf chunk size. */
|
|
1635
|
+
private resolveLeafChunkTokensForBand(
|
|
1636
|
+
band: ActivityBand,
|
|
1637
|
+
bounds: DynamicLeafChunkBounds,
|
|
1638
|
+
): number {
|
|
1639
|
+
switch (band) {
|
|
1640
|
+
case "high":
|
|
1641
|
+
return bounds.high;
|
|
1642
|
+
case "medium":
|
|
1643
|
+
return bounds.medium;
|
|
1644
|
+
default:
|
|
1645
|
+
return bounds.floor;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/** Build descending fallback chunk sizes used when a provider rejects a larger chunk. */
|
|
1650
|
+
private buildLeafChunkFallbacks(params: {
|
|
1651
|
+
preferred: number;
|
|
1652
|
+
bounds: DynamicLeafChunkBounds;
|
|
1653
|
+
}): number[] {
|
|
1654
|
+
const ordered = [params.preferred, params.bounds.max, params.bounds.high, params.bounds.medium, params.bounds.floor];
|
|
1655
|
+
const seen = new Set<number>();
|
|
1656
|
+
const fallbacks: number[] = [];
|
|
1657
|
+
for (const value of ordered) {
|
|
1658
|
+
const normalized = Math.max(params.bounds.floor, Math.floor(value));
|
|
1659
|
+
if (seen.has(normalized)) {
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
seen.add(normalized);
|
|
1663
|
+
fallbacks.push(normalized);
|
|
1664
|
+
}
|
|
1665
|
+
return fallbacks.sort((a, b) => b - a);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/** Detect provider/model token-limit failures that should trigger a lower chunk retry. */
|
|
1669
|
+
private isRecoverableLeafChunkOverflowError(error: unknown): boolean {
|
|
1670
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
1671
|
+
if (!message) {
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
return [
|
|
1675
|
+
"context length",
|
|
1676
|
+
"context window",
|
|
1677
|
+
"maximum context",
|
|
1678
|
+
"max context",
|
|
1679
|
+
"too many tokens",
|
|
1680
|
+
"too many input tokens",
|
|
1681
|
+
"input tokens",
|
|
1682
|
+
"token limit",
|
|
1683
|
+
"context limit",
|
|
1684
|
+
"input is too large",
|
|
1685
|
+
"input too large",
|
|
1686
|
+
"prompt is too long",
|
|
1687
|
+
"request too large",
|
|
1688
|
+
"exceeds the model",
|
|
1689
|
+
"exceeds context",
|
|
1690
|
+
].some((fragment) => message.includes(fragment));
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/** Extract the current prompt-cache snapshot from runtime context, if present. */
|
|
1694
|
+
private readPromptCacheSnapshot(runtimeContext?: Record<string, unknown>): PromptCacheSnapshot | null {
|
|
1695
|
+
const promptCache = asRecord(runtimeContext?.promptCache);
|
|
1696
|
+
if (!promptCache) {
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const lastCallUsage = asRecord(promptCache.lastCallUsage);
|
|
1701
|
+
const observation = asRecord(promptCache.observation);
|
|
1702
|
+
const cacheRead = this.normalizeOptionalCount(lastCallUsage?.cacheRead);
|
|
1703
|
+
const cacheWrite = this.normalizeOptionalCount(lastCallUsage?.cacheWrite);
|
|
1704
|
+
const sawExplicitBreak = safeBoolean(observation?.broke) === true;
|
|
1705
|
+
const retention = safeString(promptCache.retention)?.trim();
|
|
1706
|
+
const hasUsageSignal = cacheRead !== undefined || cacheWrite !== undefined;
|
|
1707
|
+
const hasObservationSignal =
|
|
1708
|
+
typeof observation?.cacheRead === "number"
|
|
1709
|
+
|| typeof observation?.previousCacheRead === "number"
|
|
1710
|
+
|| sawExplicitBreak;
|
|
1711
|
+
|
|
1712
|
+
let cacheState: CacheState = "unknown";
|
|
1713
|
+
if (sawExplicitBreak) {
|
|
1714
|
+
cacheState = "cold";
|
|
1715
|
+
} else if (typeof cacheRead === "number" && cacheRead > 0) {
|
|
1716
|
+
cacheState = "hot";
|
|
1717
|
+
} else if (hasUsageSignal || hasObservationSignal) {
|
|
1718
|
+
cacheState = "cold";
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
return {
|
|
1722
|
+
...(cacheRead !== undefined ? { lastObservedCacheRead: cacheRead } : {}),
|
|
1723
|
+
...(cacheWrite !== undefined ? { lastObservedCacheWrite: cacheWrite } : {}),
|
|
1724
|
+
cacheState,
|
|
1725
|
+
...(retention ? { retention } : {}),
|
|
1726
|
+
sawExplicitBreak,
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/** Persist the current turn's compaction telemetry for later policy decisions. */
|
|
1731
|
+
private async updateCompactionTelemetry(params: {
|
|
1732
|
+
conversationId: number;
|
|
1733
|
+
runtimeContext?: Record<string, unknown>;
|
|
1734
|
+
tokenBudget?: number;
|
|
1735
|
+
rawTokensOutsideTail?: number;
|
|
1736
|
+
}): Promise<ConversationCompactionTelemetryRecord | null> {
|
|
1737
|
+
const snapshot = this.readPromptCacheSnapshot(params.runtimeContext);
|
|
1738
|
+
const existing = await this.compactionTelemetryStore.getConversationCompactionTelemetry(
|
|
1739
|
+
params.conversationId,
|
|
1740
|
+
);
|
|
1741
|
+
if (!snapshot && params.rawTokensOutsideTail === undefined) {
|
|
1742
|
+
return existing;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const now = new Date();
|
|
1746
|
+
const bounds = this.resolveDynamicLeafChunkBounds(params.tokenBudget);
|
|
1747
|
+
const turnsSinceLeafCompaction =
|
|
1748
|
+
(existing?.turnsSinceLeafCompaction ?? 0) + 1;
|
|
1749
|
+
const tokensAccumulatedSinceLeafCompaction =
|
|
1750
|
+
params.rawTokensOutsideTail ?? existing?.tokensAccumulatedSinceLeafCompaction ?? 0;
|
|
1751
|
+
const lastActivityBand = this.classifyDynamicLeafActivityBand({
|
|
1752
|
+
lastActivityBand: existing?.lastActivityBand,
|
|
1753
|
+
tokensAccumulatedSinceLeafCompaction,
|
|
1754
|
+
turnsSinceLeafCompaction,
|
|
1755
|
+
floor: bounds.floor,
|
|
1756
|
+
});
|
|
1757
|
+
await this.compactionTelemetryStore.upsertConversationCompactionTelemetry({
|
|
1758
|
+
conversationId: params.conversationId,
|
|
1759
|
+
lastObservedCacheRead: snapshot?.lastObservedCacheRead ?? existing?.lastObservedCacheRead ?? null,
|
|
1760
|
+
lastObservedCacheWrite:
|
|
1761
|
+
snapshot?.lastObservedCacheWrite ?? existing?.lastObservedCacheWrite ?? null,
|
|
1762
|
+
lastObservedCacheHitAt:
|
|
1763
|
+
snapshot?.cacheState === "hot"
|
|
1764
|
+
? now
|
|
1765
|
+
: existing?.lastObservedCacheHitAt ?? null,
|
|
1766
|
+
lastObservedCacheBreakAt:
|
|
1767
|
+
snapshot?.sawExplicitBreak
|
|
1768
|
+
? now
|
|
1769
|
+
: existing?.lastObservedCacheBreakAt ?? null,
|
|
1770
|
+
cacheState: snapshot?.cacheState ?? existing?.cacheState ?? "unknown",
|
|
1771
|
+
retention: snapshot?.retention ?? existing?.retention ?? null,
|
|
1772
|
+
lastLeafCompactionAt: existing?.lastLeafCompactionAt ?? null,
|
|
1773
|
+
turnsSinceLeafCompaction,
|
|
1774
|
+
tokensAccumulatedSinceLeafCompaction,
|
|
1775
|
+
lastActivityBand,
|
|
1776
|
+
});
|
|
1777
|
+
const updated = await this.compactionTelemetryStore.getConversationCompactionTelemetry(
|
|
1778
|
+
params.conversationId,
|
|
1779
|
+
);
|
|
1780
|
+
if (updated) {
|
|
1781
|
+
this.deps.log.debug(
|
|
1782
|
+
`[lcm] compaction telemetry updated: conversation=${params.conversationId} cacheState=${updated.cacheState} cacheRead=${updated.lastObservedCacheRead ?? "null"} cacheWrite=${updated.lastObservedCacheWrite ?? "null"} retention=${updated.retention ?? "null"} turnsSinceLeafCompaction=${updated.turnsSinceLeafCompaction} tokensSinceLeafCompaction=${updated.tokensAccumulatedSinceLeafCompaction} activityBand=${updated.lastActivityBand} rawTokensOutsideTail=${params.rawTokensOutsideTail ?? "null"} tokenBudget=${params.tokenBudget ?? "null"}`,
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
return updated;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/** Reset refill counters after any successful leaf-producing compaction. */
|
|
1789
|
+
private async markLeafCompactionTelemetrySuccess(params: {
|
|
1790
|
+
conversationId: number;
|
|
1791
|
+
activityBand?: ActivityBand;
|
|
1792
|
+
}): Promise<void> {
|
|
1793
|
+
const existing = await this.compactionTelemetryStore.getConversationCompactionTelemetry(
|
|
1794
|
+
params.conversationId,
|
|
1795
|
+
);
|
|
1796
|
+
await this.compactionTelemetryStore.upsertConversationCompactionTelemetry({
|
|
1797
|
+
conversationId: params.conversationId,
|
|
1798
|
+
lastObservedCacheRead: existing?.lastObservedCacheRead ?? null,
|
|
1799
|
+
lastObservedCacheWrite: existing?.lastObservedCacheWrite ?? null,
|
|
1800
|
+
lastObservedCacheHitAt: existing?.lastObservedCacheHitAt ?? null,
|
|
1801
|
+
lastObservedCacheBreakAt: existing?.lastObservedCacheBreakAt ?? null,
|
|
1802
|
+
cacheState: existing?.cacheState ?? "unknown",
|
|
1803
|
+
retention: existing?.retention ?? null,
|
|
1804
|
+
lastLeafCompactionAt: new Date(),
|
|
1805
|
+
turnsSinceLeafCompaction: 0,
|
|
1806
|
+
tokensAccumulatedSinceLeafCompaction: 0,
|
|
1807
|
+
lastActivityBand: params.activityBand ?? existing?.lastActivityBand ?? "low",
|
|
1808
|
+
});
|
|
1809
|
+
this.deps.log.debug(
|
|
1810
|
+
`[lcm] compaction telemetry reset after leaf compaction: conversation=${params.conversationId} cacheState=${existing?.cacheState ?? "unknown"} activityBand=${params.activityBand ?? existing?.lastActivityBand ?? "low"}`,
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/** Emit an operational trace for the incremental compaction policy decision. */
|
|
1815
|
+
private logIncrementalCompactionDecision(params: {
|
|
1816
|
+
conversationId: number;
|
|
1817
|
+
cacheState: CacheState;
|
|
1818
|
+
activityBand: ActivityBand;
|
|
1819
|
+
triggerLeafChunkTokens: number;
|
|
1820
|
+
preferredLeafChunkTokens: number;
|
|
1821
|
+
fallbackLeafChunkTokens: number[];
|
|
1822
|
+
rawTokensOutsideTail: number;
|
|
1823
|
+
threshold: number;
|
|
1824
|
+
shouldCompact: boolean;
|
|
1825
|
+
maxPasses: number;
|
|
1826
|
+
allowCondensedPasses: boolean;
|
|
1827
|
+
reason: string;
|
|
1828
|
+
}): IncrementalCompactionDecision {
|
|
1829
|
+
this.deps.log.info(
|
|
1830
|
+
`[lcm] incremental compaction decision: conversation=${params.conversationId} cacheState=${params.cacheState} activityBand=${params.activityBand} triggerLeafChunkTokens=${params.triggerLeafChunkTokens} preferredLeafChunkTokens=${params.preferredLeafChunkTokens} fallbackLeafChunkTokens=${params.fallbackLeafChunkTokens.join(",")} rawTokensOutsideTail=${params.rawTokensOutsideTail} threshold=${params.threshold} shouldCompact=${params.shouldCompact} maxPasses=${params.maxPasses} allowCondensedPasses=${params.allowCondensedPasses} reason=${params.reason}`,
|
|
1831
|
+
);
|
|
1832
|
+
return {
|
|
1833
|
+
shouldCompact: params.shouldCompact,
|
|
1834
|
+
cacheState: params.cacheState,
|
|
1835
|
+
maxPasses: params.maxPasses,
|
|
1836
|
+
rawTokensOutsideTail: params.rawTokensOutsideTail,
|
|
1837
|
+
threshold: params.threshold,
|
|
1838
|
+
leafChunkTokens: params.preferredLeafChunkTokens,
|
|
1839
|
+
fallbackLeafChunkTokens: params.fallbackLeafChunkTokens,
|
|
1840
|
+
activityBand: params.activityBand,
|
|
1841
|
+
allowCondensedPasses: params.allowCondensedPasses,
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/** Resolve the cache-aware incremental-compaction policy for the current session. */
|
|
1846
|
+
private async evaluateIncrementalCompaction(params: {
|
|
1847
|
+
conversationId: number;
|
|
1848
|
+
tokenBudget: number;
|
|
1849
|
+
currentTokenCount?: number;
|
|
1850
|
+
}): Promise<IncrementalCompactionDecision> {
|
|
1851
|
+
const telemetry = await this.compactionTelemetryStore.getConversationCompactionTelemetry(
|
|
1852
|
+
params.conversationId,
|
|
1853
|
+
);
|
|
1854
|
+
const cacheState =
|
|
1855
|
+
this.config.cacheAwareCompaction.enabled
|
|
1856
|
+
? this.resolveCacheAwareState(telemetry)
|
|
1857
|
+
: "unknown";
|
|
1858
|
+
const bounds = this.resolveDynamicLeafChunkBounds(params.tokenBudget);
|
|
1859
|
+
const activityBand =
|
|
1860
|
+
this.config.dynamicLeafChunkTokens.enabled
|
|
1861
|
+
? this.classifyDynamicLeafActivityBand({
|
|
1862
|
+
lastActivityBand: telemetry?.lastActivityBand,
|
|
1863
|
+
tokensAccumulatedSinceLeafCompaction:
|
|
1864
|
+
telemetry?.tokensAccumulatedSinceLeafCompaction ?? 0,
|
|
1865
|
+
turnsSinceLeafCompaction: telemetry?.turnsSinceLeafCompaction ?? 0,
|
|
1866
|
+
floor: bounds.floor,
|
|
1867
|
+
})
|
|
1868
|
+
: "low";
|
|
1869
|
+
const triggerLeafChunkTokens =
|
|
1870
|
+
this.config.dynamicLeafChunkTokens.enabled && cacheState === "hot"
|
|
1871
|
+
? bounds.max
|
|
1872
|
+
: this.config.dynamicLeafChunkTokens.enabled
|
|
1873
|
+
? this.resolveLeafChunkTokensForBand(activityBand, bounds)
|
|
1874
|
+
: bounds.floor;
|
|
1875
|
+
const preferredLeafChunkTokens =
|
|
1876
|
+
this.config.cacheAwareCompaction.enabled && (cacheState === "cold" || cacheState === "hot")
|
|
1877
|
+
? bounds.max
|
|
1878
|
+
: triggerLeafChunkTokens;
|
|
1879
|
+
const fallbackLeafChunkTokens = this.buildLeafChunkFallbacks({
|
|
1880
|
+
preferred: preferredLeafChunkTokens,
|
|
1881
|
+
bounds,
|
|
1882
|
+
});
|
|
1883
|
+
const leafTrigger = await this.compaction.evaluateLeafTrigger(
|
|
1884
|
+
params.conversationId,
|
|
1885
|
+
triggerLeafChunkTokens,
|
|
1886
|
+
);
|
|
1887
|
+
if (!leafTrigger.shouldCompact) {
|
|
1888
|
+
return this.logIncrementalCompactionDecision({
|
|
1889
|
+
conversationId: params.conversationId,
|
|
1890
|
+
cacheState,
|
|
1891
|
+
activityBand,
|
|
1892
|
+
triggerLeafChunkTokens,
|
|
1893
|
+
preferredLeafChunkTokens,
|
|
1894
|
+
fallbackLeafChunkTokens,
|
|
1895
|
+
rawTokensOutsideTail: leafTrigger.rawTokensOutsideTail,
|
|
1896
|
+
threshold: leafTrigger.threshold,
|
|
1897
|
+
shouldCompact: false,
|
|
1898
|
+
maxPasses: 1,
|
|
1899
|
+
allowCondensedPasses: false,
|
|
1900
|
+
reason: "below-leaf-trigger",
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const budgetDecision = await this.compaction.evaluate(
|
|
1905
|
+
params.conversationId,
|
|
1906
|
+
params.tokenBudget,
|
|
1907
|
+
params.currentTokenCount,
|
|
1908
|
+
);
|
|
1909
|
+
if (budgetDecision.shouldCompact) {
|
|
1910
|
+
return this.logIncrementalCompactionDecision({
|
|
1911
|
+
conversationId: params.conversationId,
|
|
1912
|
+
cacheState,
|
|
1913
|
+
activityBand,
|
|
1914
|
+
triggerLeafChunkTokens,
|
|
1915
|
+
preferredLeafChunkTokens,
|
|
1916
|
+
fallbackLeafChunkTokens,
|
|
1917
|
+
rawTokensOutsideTail: leafTrigger.rawTokensOutsideTail,
|
|
1918
|
+
threshold: leafTrigger.threshold,
|
|
1919
|
+
shouldCompact: true,
|
|
1920
|
+
maxPasses: 1,
|
|
1921
|
+
allowCondensedPasses: true,
|
|
1922
|
+
reason: "budget-trigger",
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
if (
|
|
1927
|
+
cacheState === "hot"
|
|
1928
|
+
&& this.isComfortablyUnderTokenBudget({
|
|
1929
|
+
currentTokenCount: params.currentTokenCount,
|
|
1930
|
+
tokenBudget: params.tokenBudget,
|
|
1931
|
+
})
|
|
1932
|
+
) {
|
|
1933
|
+
return this.logIncrementalCompactionDecision({
|
|
1934
|
+
conversationId: params.conversationId,
|
|
1935
|
+
cacheState,
|
|
1936
|
+
activityBand,
|
|
1937
|
+
triggerLeafChunkTokens,
|
|
1938
|
+
preferredLeafChunkTokens,
|
|
1939
|
+
fallbackLeafChunkTokens,
|
|
1940
|
+
rawTokensOutsideTail: leafTrigger.rawTokensOutsideTail,
|
|
1941
|
+
threshold: leafTrigger.threshold,
|
|
1942
|
+
shouldCompact: false,
|
|
1943
|
+
maxPasses: 1,
|
|
1944
|
+
allowCondensedPasses: false,
|
|
1945
|
+
reason: "hot-cache-budget-headroom",
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
if (
|
|
1950
|
+
cacheState === "hot"
|
|
1951
|
+
&& leafTrigger.rawTokensOutsideTail
|
|
1952
|
+
< Math.floor(
|
|
1953
|
+
leafTrigger.threshold * this.config.cacheAwareCompaction.hotCachePressureFactor,
|
|
1954
|
+
)
|
|
1955
|
+
) {
|
|
1956
|
+
return this.logIncrementalCompactionDecision({
|
|
1957
|
+
conversationId: params.conversationId,
|
|
1958
|
+
cacheState,
|
|
1959
|
+
activityBand,
|
|
1960
|
+
triggerLeafChunkTokens,
|
|
1961
|
+
preferredLeafChunkTokens,
|
|
1962
|
+
fallbackLeafChunkTokens,
|
|
1963
|
+
rawTokensOutsideTail: leafTrigger.rawTokensOutsideTail,
|
|
1964
|
+
threshold: leafTrigger.threshold,
|
|
1965
|
+
shouldCompact: false,
|
|
1966
|
+
maxPasses: 1,
|
|
1967
|
+
allowCondensedPasses: false,
|
|
1968
|
+
reason: "hot-cache-defer",
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
const maxPasses =
|
|
1973
|
+
cacheState === "cold"
|
|
1974
|
+
? Math.max(1, this.config.cacheAwareCompaction.maxColdCacheCatchupPasses)
|
|
1975
|
+
: 1;
|
|
1976
|
+
return this.logIncrementalCompactionDecision({
|
|
1977
|
+
conversationId: params.conversationId,
|
|
1978
|
+
cacheState,
|
|
1979
|
+
activityBand,
|
|
1980
|
+
triggerLeafChunkTokens,
|
|
1981
|
+
preferredLeafChunkTokens,
|
|
1982
|
+
fallbackLeafChunkTokens,
|
|
1983
|
+
rawTokensOutsideTail: leafTrigger.rawTokensOutsideTail,
|
|
1984
|
+
threshold: leafTrigger.threshold,
|
|
1985
|
+
shouldCompact: true,
|
|
1986
|
+
maxPasses,
|
|
1987
|
+
allowCondensedPasses: cacheState !== "hot",
|
|
1988
|
+
reason: cacheState === "cold" ? "cold-cache-catchup" : "leaf-trigger",
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1449
1992
|
/** Resolve an LCM conversation id from a session key via the session store. */
|
|
1450
1993
|
private async resolveConversationIdForSessionKey(
|
|
1451
1994
|
sessionKey: string,
|
|
@@ -1475,6 +2018,23 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1475
2018
|
}
|
|
1476
2019
|
}
|
|
1477
2020
|
|
|
2021
|
+
/** Format stable session identifiers for LCM diagnostic logs. */
|
|
2022
|
+
private formatSessionLogContext(params: {
|
|
2023
|
+
conversationId: number;
|
|
2024
|
+
sessionId: string;
|
|
2025
|
+
sessionKey?: string;
|
|
2026
|
+
}): string {
|
|
2027
|
+
const parts = [
|
|
2028
|
+
`conversation=${params.conversationId}`,
|
|
2029
|
+
`session=${params.sessionId}`,
|
|
2030
|
+
];
|
|
2031
|
+
const trimmedSessionKey = params.sessionKey?.trim();
|
|
2032
|
+
if (trimmedSessionKey) {
|
|
2033
|
+
parts.push(`sessionKey=${trimmedSessionKey}`);
|
|
2034
|
+
}
|
|
2035
|
+
return parts.join(" ");
|
|
2036
|
+
}
|
|
2037
|
+
|
|
1478
2038
|
/** Build a summarize callback with runtime provider fallback handling. */
|
|
1479
2039
|
private async resolveSummarize(params: {
|
|
1480
2040
|
legacyParams?: Record<string, unknown>;
|
|
@@ -1510,11 +2070,13 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1510
2070
|
breakerKey: runtimeSummarizer.breakerKey,
|
|
1511
2071
|
};
|
|
1512
2072
|
}
|
|
1513
|
-
|
|
2073
|
+
this.deps.log.error(`[lcm] resolveSummarize: createLcmSummarizeFromLegacyParams returned undefined`);
|
|
1514
2074
|
} catch (err) {
|
|
1515
|
-
|
|
2075
|
+
this.deps.log.error(
|
|
2076
|
+
`[lcm] resolveSummarize failed, using emergency fallback: ${describeLogError(err)}`,
|
|
2077
|
+
);
|
|
1516
2078
|
}
|
|
1517
|
-
|
|
2079
|
+
this.deps.log.error(`[lcm] resolveSummarize: FALLING BACK TO EMERGENCY TRUNCATION`);
|
|
1518
2080
|
return { summarize: createEmergencyFallbackSummarize(), summaryModel: "unknown" };
|
|
1519
2081
|
}
|
|
1520
2082
|
|
|
@@ -1947,7 +2509,14 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1947
2509
|
|
|
1948
2510
|
const existingDbCount = await this.conversationStore.getMessageCount(conversationId);
|
|
1949
2511
|
if (existingDbCount > 0 && missingTail.length > Math.max(existingDbCount * 0.2, 50)) {
|
|
1950
|
-
|
|
2512
|
+
const sessionContext = this.formatSessionLogContext({
|
|
2513
|
+
conversationId,
|
|
2514
|
+
sessionId,
|
|
2515
|
+
sessionKey: params.sessionKey,
|
|
2516
|
+
});
|
|
2517
|
+
this.deps.log.warn(
|
|
2518
|
+
`[lcm] reconcileSessionTail: import cap exceeded for ${sessionContext} — would import ${missingTail.length} messages (existing: ${existingDbCount}). Aborting to prevent flood.`,
|
|
2519
|
+
);
|
|
1951
2520
|
return { blockedByImportCap: true, importedMessages: 0, hasOverlap: true };
|
|
1952
2521
|
}
|
|
1953
2522
|
|
|
@@ -1962,6 +2531,37 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1962
2531
|
return { blockedByImportCap: false, importedMessages, hasOverlap: true };
|
|
1963
2532
|
}
|
|
1964
2533
|
|
|
2534
|
+
/**
|
|
2535
|
+
* Persist bootstrap checkpoint metadata anchored to the current DB frontier.
|
|
2536
|
+
*
|
|
2537
|
+
* We intentionally checkpoint the session file's current EOF while hashing the
|
|
2538
|
+
* latest persisted DB message. This keeps append-only recovery aligned with the
|
|
2539
|
+
* canonical LCM frontier even when trailing transcript entries are pruned or
|
|
2540
|
+
* otherwise noncanonical.
|
|
2541
|
+
*/
|
|
2542
|
+
private async refreshBootstrapState(params: {
|
|
2543
|
+
conversationId: number;
|
|
2544
|
+
sessionFile: string;
|
|
2545
|
+
fileStats?: { size: number; mtimeMs: number };
|
|
2546
|
+
}): Promise<void> {
|
|
2547
|
+
const latestDbMessage = await this.conversationStore.getLastMessage(params.conversationId);
|
|
2548
|
+
const fileStats = params.fileStats ?? statSync(params.sessionFile);
|
|
2549
|
+
await this.summaryStore.upsertConversationBootstrapState({
|
|
2550
|
+
conversationId: params.conversationId,
|
|
2551
|
+
sessionFilePath: params.sessionFile,
|
|
2552
|
+
lastSeenSize: fileStats.size,
|
|
2553
|
+
lastSeenMtimeMs: Math.trunc(fileStats.mtimeMs),
|
|
2554
|
+
lastProcessedOffset: fileStats.size,
|
|
2555
|
+
lastProcessedEntryHash: latestDbMessage
|
|
2556
|
+
? createBootstrapEntryHash({
|
|
2557
|
+
role: latestDbMessage.role,
|
|
2558
|
+
content: latestDbMessage.content,
|
|
2559
|
+
tokenCount: latestDbMessage.tokenCount,
|
|
2560
|
+
})
|
|
2561
|
+
: null,
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
|
|
1965
2565
|
async bootstrap(params: {
|
|
1966
2566
|
sessionId: string;
|
|
1967
2567
|
sessionFile: string;
|
|
@@ -1992,19 +2592,14 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
1992
2592
|
this.conversationStore.withTransaction(async () => {
|
|
1993
2593
|
const persistBootstrapState = async (
|
|
1994
2594
|
conversationId: number,
|
|
1995
|
-
historicalMessages: AgentMessage[],
|
|
1996
2595
|
): Promise<void> => {
|
|
1997
|
-
|
|
1998
|
-
historicalMessages.length > 0
|
|
1999
|
-
? toStoredMessage(historicalMessages[historicalMessages.length - 1]!)
|
|
2000
|
-
: null;
|
|
2001
|
-
await this.summaryStore.upsertConversationBootstrapState({
|
|
2596
|
+
await this.refreshBootstrapState({
|
|
2002
2597
|
conversationId,
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2598
|
+
sessionFile: params.sessionFile,
|
|
2599
|
+
fileStats: {
|
|
2600
|
+
size: sessionFileSize,
|
|
2601
|
+
mtimeMs: sessionFileMtimeMs,
|
|
2602
|
+
},
|
|
2008
2603
|
});
|
|
2009
2604
|
};
|
|
2010
2605
|
|
|
@@ -2055,6 +2650,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2055
2650
|
params.sessionFile,
|
|
2056
2651
|
bootstrapState.lastProcessedOffset,
|
|
2057
2652
|
true,
|
|
2653
|
+
(message) => createBootstrapEntryHash(toStoredMessage(message)) === latestDbHash,
|
|
2058
2654
|
);
|
|
2059
2655
|
const tailEntryMessage = readBootstrapMessageFromJsonLine(tailEntryRaw);
|
|
2060
2656
|
const tailEntryHash = tailEntryMessage
|
|
@@ -2088,14 +2684,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2088
2684
|
}
|
|
2089
2685
|
}
|
|
2090
2686
|
|
|
2091
|
-
|
|
2092
|
-
appended.messages.length > 0
|
|
2093
|
-
? appended.messages[appended.messages.length - 1]!
|
|
2094
|
-
: tailEntryMessage;
|
|
2095
|
-
await persistBootstrapState(
|
|
2096
|
-
conversationId,
|
|
2097
|
-
lastAppendedMessage ? [lastAppendedMessage] : [],
|
|
2098
|
-
);
|
|
2687
|
+
await persistBootstrapState(conversationId);
|
|
2099
2688
|
|
|
2100
2689
|
if (importedMessages > 0) {
|
|
2101
2690
|
return {
|
|
@@ -2126,7 +2715,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2126
2715
|
|
|
2127
2716
|
if (bootstrapMessages.length === 0) {
|
|
2128
2717
|
await this.conversationStore.markConversationBootstrapped(conversationId);
|
|
2129
|
-
await persistBootstrapState(conversationId
|
|
2718
|
+
await persistBootstrapState(conversationId);
|
|
2130
2719
|
return {
|
|
2131
2720
|
bootstrapped: false,
|
|
2132
2721
|
importedMessages: 0,
|
|
@@ -2152,18 +2741,19 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2152
2741
|
inserted.map((record) => record.messageId),
|
|
2153
2742
|
);
|
|
2154
2743
|
await this.conversationStore.markConversationBootstrapped(conversationId);
|
|
2155
|
-
await persistBootstrapState(conversationId, historicalMessages);
|
|
2156
2744
|
|
|
2157
2745
|
// Prune HEARTBEAT_OK turns from the freshly imported data
|
|
2158
2746
|
if (this.config.pruneHeartbeatOk) {
|
|
2159
2747
|
const pruned = await this.pruneHeartbeatOkTurns(conversationId);
|
|
2160
2748
|
if (pruned > 0) {
|
|
2161
|
-
|
|
2749
|
+
this.deps.log.info(
|
|
2162
2750
|
`[lcm] bootstrap: pruned ${pruned} HEARTBEAT_OK messages from conversation ${conversationId}`,
|
|
2163
2751
|
);
|
|
2164
2752
|
}
|
|
2165
2753
|
}
|
|
2166
2754
|
|
|
2755
|
+
await persistBootstrapState(conversationId);
|
|
2756
|
+
|
|
2167
2757
|
return {
|
|
2168
2758
|
bootstrapped: true,
|
|
2169
2759
|
importedMessages: inserted.length,
|
|
@@ -2192,7 +2782,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2192
2782
|
}
|
|
2193
2783
|
|
|
2194
2784
|
if (reconcile.importedMessages > 0) {
|
|
2195
|
-
await persistBootstrapState(conversationId
|
|
2785
|
+
await persistBootstrapState(conversationId);
|
|
2196
2786
|
return {
|
|
2197
2787
|
bootstrapped: true,
|
|
2198
2788
|
importedMessages: reconcile.importedMessages,
|
|
@@ -2201,7 +2791,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2201
2791
|
}
|
|
2202
2792
|
|
|
2203
2793
|
if (reconcile.hasOverlap) {
|
|
2204
|
-
await persistBootstrapState(conversationId
|
|
2794
|
+
await persistBootstrapState(conversationId);
|
|
2205
2795
|
}
|
|
2206
2796
|
|
|
2207
2797
|
if (conversation.bootstrappedAt) {
|
|
@@ -2233,15 +2823,18 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2233
2823
|
if (conversation) {
|
|
2234
2824
|
const pruned = await this.pruneHeartbeatOkTurns(conversation.conversationId);
|
|
2235
2825
|
if (pruned > 0) {
|
|
2236
|
-
|
|
2826
|
+
await this.refreshBootstrapState({
|
|
2827
|
+
conversationId: conversation.conversationId,
|
|
2828
|
+
sessionFile: params.sessionFile,
|
|
2829
|
+
});
|
|
2830
|
+
this.deps.log.info(
|
|
2237
2831
|
`[lcm] bootstrap: retroactively pruned ${pruned} HEARTBEAT_OK messages from conversation ${conversation.conversationId}`,
|
|
2238
2832
|
);
|
|
2239
2833
|
}
|
|
2240
2834
|
}
|
|
2241
2835
|
} catch (err) {
|
|
2242
|
-
|
|
2243
|
-
`[lcm] bootstrap: heartbeat pruning failed
|
|
2244
|
-
err instanceof Error ? err.message : err,
|
|
2836
|
+
this.deps.log.warn(
|
|
2837
|
+
`[lcm] bootstrap: heartbeat pruning failed: ${describeLogError(err)}`,
|
|
2245
2838
|
);
|
|
2246
2839
|
}
|
|
2247
2840
|
}
|
|
@@ -2455,24 +3048,14 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2455
3048
|
|
|
2456
3049
|
if (result.changed) {
|
|
2457
3050
|
try {
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
const lastEntryMsg = readBootstrapMessageFromJsonLine(lastEntryRaw);
|
|
2463
|
-
const lastEntryHash = lastEntryMsg ? createBootstrapEntryHash(toStoredMessage(lastEntryMsg)) : null;
|
|
2464
|
-
if (lastEntryHash) {
|
|
2465
|
-
await this.summaryStore.upsertConversationBootstrapState({
|
|
2466
|
-
conversationId: conversation.conversationId,
|
|
2467
|
-
sessionFilePath: params.sessionFile,
|
|
2468
|
-
lastSeenSize: newSize,
|
|
2469
|
-
lastSeenMtimeMs: newMtimeMs,
|
|
2470
|
-
lastProcessedOffset: newSize,
|
|
2471
|
-
lastProcessedEntryHash: lastEntryHash,
|
|
2472
|
-
});
|
|
2473
|
-
}
|
|
3051
|
+
await this.refreshBootstrapState({
|
|
3052
|
+
conversationId: conversation.conversationId,
|
|
3053
|
+
sessionFile: params.sessionFile,
|
|
3054
|
+
});
|
|
2474
3055
|
} catch (e) {
|
|
2475
|
-
|
|
3056
|
+
this.deps.log.warn(
|
|
3057
|
+
`[lcm] Failed to update bootstrap checkpoint after maintain: ${describeLogError(e)}`,
|
|
3058
|
+
);
|
|
2476
3059
|
}
|
|
2477
3060
|
}
|
|
2478
3061
|
|
|
@@ -2663,9 +3246,8 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2663
3246
|
});
|
|
2664
3247
|
} catch (err) {
|
|
2665
3248
|
// Never compact a stale or partially ingested frontier.
|
|
2666
|
-
|
|
2667
|
-
`[lcm] afterTurn: ingest failed, skipping compaction
|
|
2668
|
-
err instanceof Error ? err.message : err,
|
|
3249
|
+
this.deps.log.error(
|
|
3250
|
+
`[lcm] afterTurn: ingest failed, skipping compaction: ${describeLogError(err)}`,
|
|
2669
3251
|
);
|
|
2670
3252
|
return;
|
|
2671
3253
|
}
|
|
@@ -2679,16 +3261,30 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2679
3261
|
if (conversation) {
|
|
2680
3262
|
const pruned = await this.pruneHeartbeatOkTurns(conversation.conversationId);
|
|
2681
3263
|
if (pruned > 0) {
|
|
2682
|
-
|
|
2683
|
-
|
|
3264
|
+
const sessionContext = this.formatSessionLogContext({
|
|
3265
|
+
conversationId: conversation.conversationId,
|
|
3266
|
+
sessionId: params.sessionId,
|
|
3267
|
+
sessionKey: params.sessionKey,
|
|
3268
|
+
});
|
|
3269
|
+
try {
|
|
3270
|
+
await this.refreshBootstrapState({
|
|
3271
|
+
conversationId: conversation.conversationId,
|
|
3272
|
+
sessionFile: params.sessionFile,
|
|
3273
|
+
});
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
this.deps.log.warn(
|
|
3276
|
+
`[lcm] afterTurn: heartbeat pruning checkpoint refresh failed for ${sessionContext}: ${describeLogError(err)}`,
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
this.deps.log.info(
|
|
3280
|
+
`[lcm] afterTurn: pruned ${pruned} heartbeat ack messages for ${sessionContext}`,
|
|
2684
3281
|
);
|
|
2685
3282
|
return;
|
|
2686
3283
|
}
|
|
2687
3284
|
}
|
|
2688
3285
|
} catch (err) {
|
|
2689
|
-
|
|
2690
|
-
`[lcm] afterTurn: heartbeat pruning failed
|
|
2691
|
-
err instanceof Error ? err.message : err,
|
|
3286
|
+
this.deps.log.warn(
|
|
3287
|
+
`[lcm] afterTurn: heartbeat pruning failed: ${describeLogError(err)}`,
|
|
2692
3288
|
);
|
|
2693
3289
|
}
|
|
2694
3290
|
}
|
|
@@ -2702,16 +3298,41 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2702
3298
|
});
|
|
2703
3299
|
const tokenBudget = this.applyAssemblyBudgetCap(resolvedTokenBudget ?? DEFAULT_AFTER_TURN_TOKEN_BUDGET);
|
|
2704
3300
|
if (resolvedTokenBudget === undefined) {
|
|
2705
|
-
|
|
3301
|
+
this.deps.log.warn(
|
|
2706
3302
|
`[lcm] afterTurn: tokenBudget not provided; using default ${DEFAULT_AFTER_TURN_TOKEN_BUDGET}`,
|
|
2707
3303
|
);
|
|
2708
3304
|
}
|
|
2709
3305
|
|
|
2710
3306
|
const liveContextTokens = estimateSessionTokenCountForAfterTurn(params.messages);
|
|
3307
|
+
const conversation = await this.conversationStore.getConversationForSession({
|
|
3308
|
+
sessionId: params.sessionId,
|
|
3309
|
+
sessionKey: params.sessionKey,
|
|
3310
|
+
});
|
|
3311
|
+
if (!conversation) {
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
2711
3314
|
|
|
2712
3315
|
try {
|
|
2713
|
-
const
|
|
2714
|
-
|
|
3316
|
+
const rawLeafTrigger = await this.compaction.evaluateLeafTrigger(conversation.conversationId);
|
|
3317
|
+
await this.updateCompactionTelemetry({
|
|
3318
|
+
conversationId: conversation.conversationId,
|
|
3319
|
+
runtimeContext: asRecord(params.runtimeContext),
|
|
3320
|
+
tokenBudget,
|
|
3321
|
+
rawTokensOutsideTail: rawLeafTrigger.rawTokensOutsideTail,
|
|
3322
|
+
});
|
|
3323
|
+
} catch (err) {
|
|
3324
|
+
this.deps.log.warn(
|
|
3325
|
+
`[lcm] afterTurn: compaction telemetry update failed: ${describeLogError(err)}`,
|
|
3326
|
+
);
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
try {
|
|
3330
|
+
const leafDecision = await this.evaluateIncrementalCompaction({
|
|
3331
|
+
conversationId: conversation.conversationId,
|
|
3332
|
+
tokenBudget,
|
|
3333
|
+
currentTokenCount: liveContextTokens,
|
|
3334
|
+
});
|
|
3335
|
+
if (leafDecision.shouldCompact) {
|
|
2715
3336
|
this.compactLeafAsync({
|
|
2716
3337
|
sessionId: params.sessionId,
|
|
2717
3338
|
sessionKey: params.sessionKey,
|
|
@@ -2719,6 +3340,11 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2719
3340
|
tokenBudget,
|
|
2720
3341
|
currentTokenCount: liveContextTokens,
|
|
2721
3342
|
legacyParams,
|
|
3343
|
+
maxPasses: leafDecision.maxPasses,
|
|
3344
|
+
leafChunkTokens: leafDecision.leafChunkTokens,
|
|
3345
|
+
fallbackLeafChunkTokens: leafDecision.fallbackLeafChunkTokens,
|
|
3346
|
+
activityBand: leafDecision.activityBand,
|
|
3347
|
+
allowCondensedPasses: leafDecision.allowCondensedPasses,
|
|
2722
3348
|
}).catch(() => {
|
|
2723
3349
|
// Leaf compaction is best-effort and should not fail the caller.
|
|
2724
3350
|
});
|
|
@@ -2856,7 +3482,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2856
3482
|
return this.compaction.evaluateLeafTrigger(conversation.conversationId);
|
|
2857
3483
|
}
|
|
2858
3484
|
|
|
2859
|
-
/** Run one incremental leaf compaction
|
|
3485
|
+
/** Run one or more incremental leaf compaction passes in the per-session queue. */
|
|
2860
3486
|
async compactLeafAsync(params: {
|
|
2861
3487
|
sessionId: string;
|
|
2862
3488
|
sessionKey?: string;
|
|
@@ -2870,6 +3496,11 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2870
3496
|
legacyParams?: Record<string, unknown>;
|
|
2871
3497
|
force?: boolean;
|
|
2872
3498
|
previousSummaryContent?: string;
|
|
3499
|
+
maxPasses?: number;
|
|
3500
|
+
leafChunkTokens?: number;
|
|
3501
|
+
fallbackLeafChunkTokens?: number[];
|
|
3502
|
+
activityBand?: ActivityBand;
|
|
3503
|
+
allowCondensedPasses?: boolean;
|
|
2873
3504
|
}): Promise<CompactResult> {
|
|
2874
3505
|
if (this.isStatelessSession(params.sessionKey)) {
|
|
2875
3506
|
return {
|
|
@@ -2933,38 +3564,114 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
2933
3564
|
};
|
|
2934
3565
|
}
|
|
2935
3566
|
|
|
2936
|
-
const
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3567
|
+
const storedTokensBefore = await this.summaryStore.getContextTokenCount(
|
|
3568
|
+
conversation.conversationId,
|
|
3569
|
+
);
|
|
3570
|
+
const maxPasses =
|
|
3571
|
+
typeof params.maxPasses === "number" &&
|
|
3572
|
+
Number.isFinite(params.maxPasses) &&
|
|
3573
|
+
params.maxPasses > 0
|
|
3574
|
+
? Math.floor(params.maxPasses)
|
|
3575
|
+
: 1;
|
|
3576
|
+
const fallbackLeafChunkTokens = Array.isArray(params.fallbackLeafChunkTokens)
|
|
3577
|
+
? [...new Set(params.fallbackLeafChunkTokens
|
|
3578
|
+
.filter((value): value is number => typeof value === "number" && Number.isFinite(value) && value > 0)
|
|
3579
|
+
.map((value) => Math.floor(value)))]
|
|
3580
|
+
.sort((a, b) => b - a)
|
|
3581
|
+
: [];
|
|
3582
|
+
let activeLeafChunkTokens =
|
|
3583
|
+
typeof params.leafChunkTokens === "number" &&
|
|
3584
|
+
Number.isFinite(params.leafChunkTokens) &&
|
|
3585
|
+
params.leafChunkTokens > 0
|
|
3586
|
+
? Math.floor(params.leafChunkTokens)
|
|
3587
|
+
: fallbackLeafChunkTokens[0];
|
|
3588
|
+
this.deps.log.info(
|
|
3589
|
+
`[lcm] compactLeafAsync start: conversation=${conversation.conversationId} session=${params.sessionId} leafChunkTokens=${activeLeafChunkTokens ?? "null"} fallbackLeafChunkTokens=${fallbackLeafChunkTokens.join(",")} maxPasses=${maxPasses} activityBand=${params.activityBand ?? "unknown"} allowCondensedPasses=${params.allowCondensedPasses !== false}`,
|
|
3590
|
+
);
|
|
3591
|
+
|
|
3592
|
+
let rounds = 0;
|
|
3593
|
+
let finalTokens = observedTokens ?? storedTokensBefore;
|
|
3594
|
+
let authFailure = false;
|
|
2944
3595
|
|
|
2945
|
-
|
|
3596
|
+
for (let pass = 0; pass < maxPasses; pass += 1) {
|
|
3597
|
+
let leafResult: Awaited<ReturnType<typeof this.compaction.compactLeaf>> | undefined;
|
|
3598
|
+
while (true) {
|
|
3599
|
+
try {
|
|
3600
|
+
leafResult = await this.compaction.compactLeaf({
|
|
3601
|
+
conversationId: conversation.conversationId,
|
|
3602
|
+
tokenBudget,
|
|
3603
|
+
summarize,
|
|
3604
|
+
...(activeLeafChunkTokens !== undefined ? { leafChunkTokens: activeLeafChunkTokens } : {}),
|
|
3605
|
+
force: params.force,
|
|
3606
|
+
previousSummaryContent: pass === 0 ? params.previousSummaryContent : undefined,
|
|
3607
|
+
summaryModel,
|
|
3608
|
+
allowCondensedPasses: params.allowCondensedPasses,
|
|
3609
|
+
});
|
|
3610
|
+
break;
|
|
3611
|
+
} catch (err) {
|
|
3612
|
+
const nextLeafChunkTokens = fallbackLeafChunkTokens.find(
|
|
3613
|
+
(value) => activeLeafChunkTokens !== undefined && value < activeLeafChunkTokens,
|
|
3614
|
+
);
|
|
3615
|
+
if (!this.isRecoverableLeafChunkOverflowError(err) || nextLeafChunkTokens === undefined) {
|
|
3616
|
+
throw err;
|
|
3617
|
+
}
|
|
3618
|
+
this.deps.log.warn(
|
|
3619
|
+
`[lcm] compactLeafAsync: retrying with smaller leafChunkTokens=${nextLeafChunkTokens} after provider token-limit error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3620
|
+
);
|
|
3621
|
+
activeLeafChunkTokens = nextLeafChunkTokens;
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
if (!leafResult) {
|
|
3625
|
+
break;
|
|
3626
|
+
}
|
|
3627
|
+
finalTokens = leafResult.tokensAfter;
|
|
3628
|
+
|
|
3629
|
+
if (leafResult.authFailure) {
|
|
3630
|
+
authFailure = true;
|
|
3631
|
+
break;
|
|
3632
|
+
}
|
|
3633
|
+
if (!leafResult.actionTaken) {
|
|
3634
|
+
break;
|
|
3635
|
+
}
|
|
3636
|
+
rounds += 1;
|
|
3637
|
+
if (leafResult.tokensAfter >= leafResult.tokensBefore) {
|
|
3638
|
+
break;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
if (authFailure && breakerKey) {
|
|
2946
3643
|
this.recordCompactionAuthFailure(breakerKey);
|
|
2947
|
-
} else if (
|
|
3644
|
+
} else if (rounds > 0 && breakerKey) {
|
|
2948
3645
|
this.recordCompactionSuccess(breakerKey);
|
|
2949
3646
|
}
|
|
3647
|
+
if (rounds > 0) {
|
|
3648
|
+
await this.markLeafCompactionTelemetrySuccess({
|
|
3649
|
+
conversationId: conversation.conversationId,
|
|
3650
|
+
activityBand: params.activityBand,
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
2950
3653
|
|
|
2951
|
-
const tokensBefore = observedTokens ??
|
|
3654
|
+
const tokensBefore = observedTokens ?? storedTokensBefore;
|
|
3655
|
+
this.deps.log.debug(
|
|
3656
|
+
`[lcm] compactLeafAsync result: conversation=${conversation.conversationId} session=${params.sessionId} rounds=${rounds} compacted=${rounds > 0} authFailure=${authFailure} finalLeafChunkTokens=${activeLeafChunkTokens ?? "null"} finalTokens=${finalTokens}`,
|
|
3657
|
+
);
|
|
2952
3658
|
|
|
2953
3659
|
return {
|
|
2954
3660
|
ok: true,
|
|
2955
|
-
compacted:
|
|
2956
|
-
reason:
|
|
3661
|
+
compacted: rounds > 0,
|
|
3662
|
+
reason: authFailure
|
|
2957
3663
|
? "provider auth failure"
|
|
2958
|
-
:
|
|
3664
|
+
: rounds > 0
|
|
2959
3665
|
? "compacted"
|
|
2960
3666
|
: "below threshold",
|
|
2961
3667
|
result: {
|
|
2962
3668
|
tokensBefore,
|
|
2963
|
-
tokensAfter:
|
|
3669
|
+
tokensAfter: finalTokens,
|
|
2964
3670
|
details: {
|
|
2965
|
-
rounds
|
|
3671
|
+
rounds,
|
|
2966
3672
|
targetTokens: tokenBudget,
|
|
2967
3673
|
mode: "leaf",
|
|
3674
|
+
maxPasses,
|
|
2968
3675
|
},
|
|
2969
3676
|
},
|
|
2970
3677
|
};
|
|
@@ -3093,7 +3800,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
3093
3800
|
// overflow counts can drive recovery even when persisted context is already small.
|
|
3094
3801
|
const useSweep = manualCompactionRequested || params.compactionTarget === "threshold";
|
|
3095
3802
|
if (useSweep) {
|
|
3096
|
-
const sweepResult = await this.compaction.
|
|
3803
|
+
const sweepResult = await this.compaction.compact({
|
|
3097
3804
|
conversationId,
|
|
3098
3805
|
tokenBudget,
|
|
3099
3806
|
summarize,
|
|
@@ -3107,6 +3814,9 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
3107
3814
|
} else if (sweepResult.actionTaken && breakerKey) {
|
|
3108
3815
|
this.recordCompactionSuccess(breakerKey);
|
|
3109
3816
|
}
|
|
3817
|
+
if (sweepResult.actionTaken) {
|
|
3818
|
+
await this.markLeafCompactionTelemetrySuccess({ conversationId });
|
|
3819
|
+
}
|
|
3110
3820
|
|
|
3111
3821
|
return {
|
|
3112
3822
|
ok: !sweepResult.authFailure && (sweepResult.actionTaken || !liveContextStillExceedsTarget),
|
|
@@ -3156,6 +3866,9 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
3156
3866
|
}
|
|
3157
3867
|
|
|
3158
3868
|
const didCompact = compactResult.rounds > 0;
|
|
3869
|
+
if (didCompact) {
|
|
3870
|
+
await this.markLeafCompactionTelemetrySuccess({ conversationId });
|
|
3871
|
+
}
|
|
3159
3872
|
|
|
3160
3873
|
return {
|
|
3161
3874
|
ok: compactResult.success,
|
|
@@ -3344,7 +4057,7 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
3344
4057
|
const nextSessionKey = params.nextSessionKey?.trim() || params.sessionKey?.trim() || current?.sessionKey;
|
|
3345
4058
|
const freshConversation = await this.conversationStore.createConversation({
|
|
3346
4059
|
sessionId: nextSessionId,
|
|
3347
|
-
sessionKey: nextSessionKey,
|
|
4060
|
+
...(nextSessionKey ? { sessionKey: nextSessionKey } : {}),
|
|
3348
4061
|
});
|
|
3349
4062
|
this.deps.log.info(
|
|
3350
4063
|
`[lcm] ${params.reason} lifecycle archived prior conversation and created ${freshConversation.conversationId}`,
|
|
@@ -3455,6 +4168,10 @@ export class LcmContextEngine implements ContextEngine {
|
|
|
3455
4168
|
return this.summaryStore;
|
|
3456
4169
|
}
|
|
3457
4170
|
|
|
4171
|
+
getCompactionTelemetryStore(): CompactionTelemetryStore {
|
|
4172
|
+
return this.compactionTelemetryStore;
|
|
4173
|
+
}
|
|
4174
|
+
|
|
3458
4175
|
// ── Heartbeat pruning ──────────────────────────────────────────────────
|
|
3459
4176
|
|
|
3460
4177
|
/**
|