@martian-engineering/lossless-claw 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -10
- package/docs/configuration.md +21 -0
- package/openclaw.plugin.json +39 -0
- package/package.json +1 -1
- package/src/assembler.ts +194 -3
- package/src/compaction.ts +203 -18
- package/src/db/config.ts +24 -3
- package/src/engine.ts +25 -6
- package/src/plugin/index.ts +111 -73
- package/src/store/summary-store.ts +80 -0
- package/src/summarize.ts +451 -209
- package/src/tools/lcm-expand-query-tool.ts +137 -34
- package/src/types.ts +1 -0
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
stampDelegatedExpansionContext,
|
|
22
22
|
} from "./lcm-expansion-recursion-guard.js";
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const DEFAULT_DELEGATED_WAIT_TIMEOUT_MS = 120_000;
|
|
25
25
|
const GATEWAY_TIMEOUT_MS = 10_000;
|
|
26
26
|
const DEFAULT_MAX_ANSWER_TOKENS = 2_000;
|
|
27
27
|
|
|
@@ -75,9 +75,20 @@ type ExpandQueryReply = {
|
|
|
75
75
|
truncated: boolean;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
type ParsedExpandQueryReply =
|
|
79
|
+
| {
|
|
80
|
+
ok: true;
|
|
81
|
+
value: ExpandQueryReply;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
ok: false;
|
|
85
|
+
error: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
78
88
|
type SummaryCandidate = {
|
|
79
89
|
summaryId: string;
|
|
80
90
|
conversationId: number;
|
|
91
|
+
requiresMessageExpansion: boolean;
|
|
81
92
|
};
|
|
82
93
|
|
|
83
94
|
function collectExpansionFailureText(value: unknown, parts: string[], depth = 0): void {
|
|
@@ -155,6 +166,7 @@ function shouldRetryWithoutOverride(message: string): boolean {
|
|
|
155
166
|
*/
|
|
156
167
|
function buildDelegatedExpandQueryTask(params: {
|
|
157
168
|
summaryIds: string[];
|
|
169
|
+
messageBackedSummaryIds: string[];
|
|
158
170
|
conversationId: number;
|
|
159
171
|
query?: string;
|
|
160
172
|
prompt: string;
|
|
@@ -165,6 +177,10 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
165
177
|
originSessionKey: string;
|
|
166
178
|
}) {
|
|
167
179
|
const seedSummaryIds = params.summaryIds.length > 0 ? params.summaryIds.join(", ") : "(none)";
|
|
180
|
+
const messageBackedSummaryIds =
|
|
181
|
+
params.messageBackedSummaryIds.length > 0
|
|
182
|
+
? params.messageBackedSummaryIds.join(", ")
|
|
183
|
+
: "(none)";
|
|
168
184
|
return [
|
|
169
185
|
"You are an autonomous LCM retrieval navigator. Plan and execute retrieval before answering.",
|
|
170
186
|
"",
|
|
@@ -172,6 +188,7 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
172
188
|
`Conversation scope: ${params.conversationId}`,
|
|
173
189
|
`Expansion token budget (total across this run): ${params.tokenCap}`,
|
|
174
190
|
`Seed summary IDs: ${seedSummaryIds}`,
|
|
191
|
+
`Seed summaries requiring raw message expansion: ${messageBackedSummaryIds}`,
|
|
175
192
|
params.query ? `Routing query: ${params.query}` : undefined,
|
|
176
193
|
"",
|
|
177
194
|
"Strategy:",
|
|
@@ -179,7 +196,7 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
179
196
|
"2. If additional candidates are needed, use `lcm_grep` scoped to summaries.",
|
|
180
197
|
"3. Select branches that fit remaining budget; prefer high-signal paths first.",
|
|
181
198
|
"4. Call `lcm_expand` selectively (do not expand everything blindly).",
|
|
182
|
-
"5. Keep includeMessages=false by default; use includeMessages=true
|
|
199
|
+
"5. Keep includeMessages=false by default; use includeMessages=true for the message-backed seed summaries above and any other specific leaf evidence.",
|
|
183
200
|
`6. Stay within ${params.tokenCap} total expansion tokens across all lcm_expand calls.`,
|
|
184
201
|
"",
|
|
185
202
|
"User prompt to answer:",
|
|
@@ -211,24 +228,25 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
211
228
|
].join("\n");
|
|
212
229
|
}
|
|
213
230
|
|
|
231
|
+
function formatInvalidDelegatedReply(reply: string, reason: string): string {
|
|
232
|
+
const compact = reply.replace(/\s+/g, " ").trim();
|
|
233
|
+
const snippet = compact.length <= 240 ? compact : `${compact.slice(0, 240)}...`;
|
|
234
|
+
return `Delegated expansion query returned ${reason}: ${snippet}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
214
237
|
/**
|
|
215
|
-
* Parse the child reply; accepts plain JSON or fenced JSON.
|
|
238
|
+
* Parse the child reply; accepts plain JSON or fenced JSON and rejects malformed fallbacks.
|
|
216
239
|
*/
|
|
217
240
|
function parseDelegatedExpandQueryReply(
|
|
218
241
|
rawReply: string | undefined,
|
|
219
242
|
fallbackExpandedSummaryCount: number,
|
|
220
|
-
):
|
|
221
|
-
const fallback: ExpandQueryReply = {
|
|
222
|
-
answer: (rawReply ?? "").trim(),
|
|
223
|
-
citedIds: [],
|
|
224
|
-
expandedSummaryCount: fallbackExpandedSummaryCount,
|
|
225
|
-
totalSourceTokens: 0,
|
|
226
|
-
truncated: false,
|
|
227
|
-
};
|
|
228
|
-
|
|
243
|
+
): ParsedExpandQueryReply {
|
|
229
244
|
const reply = rawReply?.trim();
|
|
230
245
|
if (!reply) {
|
|
231
|
-
return
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
error: "Delegated expansion query returned an empty reply.",
|
|
249
|
+
};
|
|
232
250
|
}
|
|
233
251
|
|
|
234
252
|
const candidates: string[] = [reply];
|
|
@@ -247,6 +265,12 @@ function parseDelegatedExpandQueryReply(
|
|
|
247
265
|
truncated?: unknown;
|
|
248
266
|
};
|
|
249
267
|
const answer = typeof parsed.answer === "string" ? parsed.answer.trim() : "";
|
|
268
|
+
if (!answer) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
error: formatInvalidDelegatedReply(reply, 'JSON without a non-empty "answer"'),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
250
274
|
const citedIds = normalizeSummaryIds(
|
|
251
275
|
Array.isArray(parsed.citedIds)
|
|
252
276
|
? parsed.citedIds.filter((value): value is string => typeof value === "string")
|
|
@@ -264,18 +288,24 @@ function parseDelegatedExpandQueryReply(
|
|
|
264
288
|
const truncated = parsed.truncated === true;
|
|
265
289
|
|
|
266
290
|
return {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
291
|
+
ok: true,
|
|
292
|
+
value: {
|
|
293
|
+
answer,
|
|
294
|
+
citedIds,
|
|
295
|
+
expandedSummaryCount,
|
|
296
|
+
totalSourceTokens,
|
|
297
|
+
truncated,
|
|
298
|
+
},
|
|
272
299
|
};
|
|
273
300
|
} catch {
|
|
274
301
|
// Try next candidate.
|
|
275
302
|
}
|
|
276
303
|
}
|
|
277
304
|
|
|
278
|
-
return
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
error: formatInvalidDelegatedReply(reply, "non-JSON output"),
|
|
308
|
+
};
|
|
279
309
|
}
|
|
280
310
|
|
|
281
311
|
/**
|
|
@@ -316,6 +346,22 @@ function resolveSourceConversationId(params: {
|
|
|
316
346
|
);
|
|
317
347
|
}
|
|
318
348
|
|
|
349
|
+
function upsertSummaryCandidate(
|
|
350
|
+
candidates: Map<string, SummaryCandidate>,
|
|
351
|
+
candidate: SummaryCandidate,
|
|
352
|
+
): void {
|
|
353
|
+
const existing = candidates.get(candidate.summaryId);
|
|
354
|
+
if (!existing) {
|
|
355
|
+
candidates.set(candidate.summaryId, candidate);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
candidates.set(candidate.summaryId, {
|
|
359
|
+
...existing,
|
|
360
|
+
requiresMessageExpansion:
|
|
361
|
+
existing.requiresMessageExpansion || candidate.requiresMessageExpansion,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
319
365
|
/**
|
|
320
366
|
* Resolve summary candidates from explicit IDs and/or query matches.
|
|
321
367
|
*/
|
|
@@ -333,13 +379,15 @@ async function resolveSummaryCandidates(params: {
|
|
|
333
379
|
if (!described || described.type !== "summary" || !described.summary) {
|
|
334
380
|
throw new Error(`Summary not found: ${summaryId}`);
|
|
335
381
|
}
|
|
336
|
-
candidates
|
|
382
|
+
upsertSummaryCandidate(candidates, {
|
|
337
383
|
summaryId,
|
|
338
384
|
conversationId: described.summary.conversationId,
|
|
385
|
+
requiresMessageExpansion: false,
|
|
339
386
|
});
|
|
340
387
|
}
|
|
341
388
|
|
|
342
389
|
if (params.query) {
|
|
390
|
+
const summaryStore = params.lcm.getSummaryStore();
|
|
343
391
|
const grepResult = await retrieval.grep({
|
|
344
392
|
query: params.query,
|
|
345
393
|
mode: "full_text",
|
|
@@ -347,11 +395,46 @@ async function resolveSummaryCandidates(params: {
|
|
|
347
395
|
conversationId: params.conversationId,
|
|
348
396
|
});
|
|
349
397
|
for (const summary of grepResult.summaries) {
|
|
350
|
-
candidates
|
|
398
|
+
upsertSummaryCandidate(candidates, {
|
|
351
399
|
summaryId: summary.summaryId,
|
|
352
400
|
conversationId: summary.conversationId,
|
|
401
|
+
requiresMessageExpansion: false,
|
|
353
402
|
});
|
|
354
403
|
}
|
|
404
|
+
|
|
405
|
+
if (grepResult.summaries.length === 0 && typeof params.conversationId === "number") {
|
|
406
|
+
const maxDepth = await summaryStore.getConversationMaxSummaryDepth(params.conversationId);
|
|
407
|
+
if (typeof maxDepth === "number" && maxDepth <= 1) {
|
|
408
|
+
const messageResult = await retrieval.grep({
|
|
409
|
+
query: params.query,
|
|
410
|
+
mode: "full_text",
|
|
411
|
+
scope: "messages",
|
|
412
|
+
conversationId: params.conversationId,
|
|
413
|
+
});
|
|
414
|
+
const messageIds = messageResult.messages.map((message) => message.messageId);
|
|
415
|
+
const leafLinks = await summaryStore.getLeafSummaryLinksForMessageIds(
|
|
416
|
+
params.conversationId,
|
|
417
|
+
messageIds,
|
|
418
|
+
);
|
|
419
|
+
const summaryIdsByMessageId = new Map<number, string[]>();
|
|
420
|
+
for (const link of leafLinks) {
|
|
421
|
+
const linkedSummaryIds = summaryIdsByMessageId.get(link.messageId) ?? [];
|
|
422
|
+
if (!linkedSummaryIds.includes(link.summaryId)) {
|
|
423
|
+
linkedSummaryIds.push(link.summaryId);
|
|
424
|
+
summaryIdsByMessageId.set(link.messageId, linkedSummaryIds);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const message of messageResult.messages) {
|
|
428
|
+
for (const summaryId of summaryIdsByMessageId.get(message.messageId) ?? []) {
|
|
429
|
+
upsertSummaryCandidate(candidates, {
|
|
430
|
+
summaryId,
|
|
431
|
+
conversationId: params.conversationId,
|
|
432
|
+
requiresMessageExpansion: true,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
355
438
|
}
|
|
356
439
|
|
|
357
440
|
return Array.from(candidates.values());
|
|
@@ -367,6 +450,10 @@ export function createLcmExpandQueryTool(input: {
|
|
|
367
450
|
/** Session key for scope fallback when sessionId is unavailable. */
|
|
368
451
|
sessionKey?: string;
|
|
369
452
|
}): AnyAgentTool {
|
|
453
|
+
const delegatedWaitTimeoutMs =
|
|
454
|
+
input.deps.config.delegationTimeoutMs || DEFAULT_DELEGATED_WAIT_TIMEOUT_MS;
|
|
455
|
+
const delegatedWaitTimeoutSeconds = Math.ceil(delegatedWaitTimeoutMs / 1000);
|
|
456
|
+
|
|
370
457
|
return {
|
|
371
458
|
name: "lcm_expand_query",
|
|
372
459
|
label: "LCM Expand Query",
|
|
@@ -505,6 +592,15 @@ export function createLcmExpandQueryTool(input: {
|
|
|
505
592
|
.filter((candidate) => candidate.conversationId === sourceConversationId)
|
|
506
593
|
.map((candidate) => candidate.summaryId),
|
|
507
594
|
);
|
|
595
|
+
const messageBackedSummaryIds = normalizeSummaryIds(
|
|
596
|
+
candidates
|
|
597
|
+
.filter(
|
|
598
|
+
(candidate) =>
|
|
599
|
+
candidate.conversationId === sourceConversationId &&
|
|
600
|
+
candidate.requiresMessageExpansion,
|
|
601
|
+
)
|
|
602
|
+
.map((candidate) => candidate.summaryId),
|
|
603
|
+
);
|
|
508
604
|
|
|
509
605
|
if (summaryIds.length === 0) {
|
|
510
606
|
return jsonResult({
|
|
@@ -520,6 +616,7 @@ export function createLcmExpandQueryTool(input: {
|
|
|
520
616
|
|
|
521
617
|
const task = buildDelegatedExpandQueryTask({
|
|
522
618
|
summaryIds,
|
|
619
|
+
messageBackedSummaryIds,
|
|
523
620
|
conversationId: sourceConversationId,
|
|
524
621
|
query: query || undefined,
|
|
525
622
|
prompt,
|
|
@@ -532,10 +629,13 @@ export function createLcmExpandQueryTool(input: {
|
|
|
532
629
|
|
|
533
630
|
const expansionProvider = input.deps.config.expansionProvider || undefined;
|
|
534
631
|
const expansionModel = input.deps.config.expansionModel || undefined;
|
|
632
|
+
const canonicalExpansionModel = expansionModel?.includes("/") ? expansionModel : undefined;
|
|
633
|
+
const delegatedOverrideProvider = canonicalExpansionModel ? undefined : expansionProvider;
|
|
634
|
+
const delegatedOverrideModel = canonicalExpansionModel || expansionModel;
|
|
535
635
|
const configuredOverrideLabel =
|
|
536
|
-
|
|
537
|
-
? `${
|
|
538
|
-
:
|
|
636
|
+
delegatedOverrideProvider && delegatedOverrideModel
|
|
637
|
+
? `${delegatedOverrideProvider}/${delegatedOverrideModel}`
|
|
638
|
+
: delegatedOverrideModel || delegatedOverrideProvider || "configured override";
|
|
539
639
|
|
|
540
640
|
const runDelegatedQuery = async (provider?: string, model?: string) => {
|
|
541
641
|
const childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
|
|
@@ -548,7 +648,7 @@ export function createLcmExpandQueryTool(input: {
|
|
|
548
648
|
issuerSessionId: callerSessionKey || "main",
|
|
549
649
|
allowedConversationIds: [sourceConversationId],
|
|
550
650
|
tokenCap: expansionTokenCap,
|
|
551
|
-
ttlMs:
|
|
651
|
+
ttlMs: delegatedWaitTimeoutMs + 30_000,
|
|
552
652
|
});
|
|
553
653
|
stampDelegatedExpansionContext({
|
|
554
654
|
sessionKey: childSessionKey,
|
|
@@ -590,9 +690,9 @@ export function createLcmExpandQueryTool(input: {
|
|
|
590
690
|
method: "agent.wait",
|
|
591
691
|
params: {
|
|
592
692
|
runId,
|
|
593
|
-
timeoutMs:
|
|
693
|
+
timeoutMs: delegatedWaitTimeoutMs,
|
|
594
694
|
},
|
|
595
|
-
timeoutMs:
|
|
695
|
+
timeoutMs: delegatedWaitTimeoutMs,
|
|
596
696
|
})) as { status?: string; error?: unknown };
|
|
597
697
|
const status = typeof wait?.status === "string" ? wait.status : "error";
|
|
598
698
|
if (status === "timeout") {
|
|
@@ -607,7 +707,7 @@ export function createLcmExpandQueryTool(input: {
|
|
|
607
707
|
runId,
|
|
608
708
|
});
|
|
609
709
|
throw new Error(
|
|
610
|
-
|
|
710
|
+
`lcm_expand_query timed out waiting for delegated expansion (${delegatedWaitTimeoutSeconds}s).`,
|
|
611
711
|
);
|
|
612
712
|
}
|
|
613
713
|
if (status !== "ok") {
|
|
@@ -623,6 +723,9 @@ export function createLcmExpandQueryTool(input: {
|
|
|
623
723
|
Array.isArray(replyPayload.messages) ? replyPayload.messages : [],
|
|
624
724
|
);
|
|
625
725
|
const parsed = parseDelegatedExpandQueryReply(reply, summaryIds.length);
|
|
726
|
+
if (!parsed.ok) {
|
|
727
|
+
throw new Error(parsed.error);
|
|
728
|
+
}
|
|
626
729
|
recordExpansionDelegationTelemetry({
|
|
627
730
|
deps: input.deps,
|
|
628
731
|
component: "lcm_expand_query",
|
|
@@ -635,12 +738,12 @@ export function createLcmExpandQueryTool(input: {
|
|
|
635
738
|
});
|
|
636
739
|
|
|
637
740
|
return jsonResult({
|
|
638
|
-
answer: parsed.answer,
|
|
639
|
-
citedIds: parsed.citedIds,
|
|
741
|
+
answer: parsed.value.answer,
|
|
742
|
+
citedIds: parsed.value.citedIds,
|
|
640
743
|
sourceConversationId,
|
|
641
|
-
expandedSummaryCount: parsed.expandedSummaryCount,
|
|
642
|
-
totalSourceTokens: parsed.totalSourceTokens,
|
|
643
|
-
truncated: parsed.truncated,
|
|
744
|
+
expandedSummaryCount: parsed.value.expandedSummaryCount,
|
|
745
|
+
totalSourceTokens: parsed.value.totalSourceTokens,
|
|
746
|
+
truncated: parsed.value.truncated,
|
|
644
747
|
});
|
|
645
748
|
} finally {
|
|
646
749
|
try {
|
|
@@ -664,7 +767,7 @@ export function createLcmExpandQueryTool(input: {
|
|
|
664
767
|
}
|
|
665
768
|
|
|
666
769
|
try {
|
|
667
|
-
return await runDelegatedQuery(
|
|
770
|
+
return await runDelegatedQuery(delegatedOverrideProvider, delegatedOverrideModel);
|
|
668
771
|
} catch (error) {
|
|
669
772
|
const failure = formatExpansionFailure(error);
|
|
670
773
|
input.deps.log.warn(
|