@martian-engineering/lossless-claw 0.5.3 → 0.6.1
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 +31 -1
- package/docs/configuration.md +23 -0
- package/openclaw.plugin.json +75 -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 +132 -36
- package/src/compaction.ts +22 -46
- package/src/db/config.ts +52 -20
- package/src/db/migration.ts +50 -13
- package/src/engine.ts +781 -172
- package/src/plugin/index.ts +45 -0
- 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 +380 -11
- package/src/summarize.ts +107 -20
- package/src/tools/lcm-expand-query-tool.ts +58 -25
- package/src/tools/lcm-expansion-recursion-guard.ts +87 -0
|
@@ -13,9 +13,11 @@ import {
|
|
|
13
13
|
resolveRequesterConversationScopeId,
|
|
14
14
|
} from "./lcm-expand-tool.delegation.js";
|
|
15
15
|
import {
|
|
16
|
+
acquireExpansionConcurrencySlot,
|
|
16
17
|
clearDelegatedExpansionContext,
|
|
17
18
|
evaluateExpansionRecursionGuard,
|
|
18
19
|
recordExpansionDelegationTelemetry,
|
|
20
|
+
releaseExpansionConcurrencySlot,
|
|
19
21
|
resolveExpansionRequestId,
|
|
20
22
|
resolveNextExpansionDepth,
|
|
21
23
|
stampDelegatedExpansionContext,
|
|
@@ -531,34 +533,36 @@ export function createLcmExpandQueryTool(input: {
|
|
|
531
533
|
});
|
|
532
534
|
}
|
|
533
535
|
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
sessionKey: input.sessionKey,
|
|
539
|
-
params: p,
|
|
540
|
-
});
|
|
541
|
-
let scopedConversationId = conversationScope.conversationId;
|
|
542
|
-
if (
|
|
543
|
-
!conversationScope.allConversations &&
|
|
544
|
-
scopedConversationId == null &&
|
|
545
|
-
callerSessionKey
|
|
546
|
-
) {
|
|
547
|
-
scopedConversationId = await resolveRequesterConversationScopeId({
|
|
548
|
-
deps: input.deps,
|
|
549
|
-
requesterSessionKey: callerSessionKey,
|
|
536
|
+
const originSessionKey = recursionCheck.originSessionKey || callerSessionKey || "main";
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const conversationScope = await resolveLcmConversationScope({
|
|
550
540
|
lcm: input.lcm,
|
|
541
|
+
deps: input.deps,
|
|
542
|
+
sessionId: input.sessionId,
|
|
543
|
+
sessionKey: input.sessionKey,
|
|
544
|
+
params: p,
|
|
551
545
|
});
|
|
552
|
-
|
|
546
|
+
let scopedConversationId = conversationScope.conversationId;
|
|
547
|
+
if (
|
|
548
|
+
!conversationScope.allConversations &&
|
|
549
|
+
scopedConversationId == null &&
|
|
550
|
+
callerSessionKey
|
|
551
|
+
) {
|
|
552
|
+
scopedConversationId = await resolveRequesterConversationScopeId({
|
|
553
|
+
deps: input.deps,
|
|
554
|
+
requesterSessionKey: callerSessionKey,
|
|
555
|
+
lcm: input.lcm,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
553
558
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
559
|
+
if (!conversationScope.allConversations && scopedConversationId == null) {
|
|
560
|
+
return jsonResult({
|
|
561
|
+
error:
|
|
562
|
+
"No LCM conversation found for this session. Provide conversationId or set allConversations=true.",
|
|
563
|
+
});
|
|
564
|
+
}
|
|
560
565
|
|
|
561
|
-
try {
|
|
562
566
|
const candidates = await resolveSummaryCandidates({
|
|
563
567
|
lcm: input.lcm,
|
|
564
568
|
explicitSummaryIds,
|
|
@@ -608,11 +612,35 @@ export function createLcmExpandQueryTool(input: {
|
|
|
608
612
|
});
|
|
609
613
|
}
|
|
610
614
|
|
|
615
|
+
const concurrencyCheck = acquireExpansionConcurrencySlot({
|
|
616
|
+
originSessionKey,
|
|
617
|
+
requestId,
|
|
618
|
+
});
|
|
619
|
+
if (concurrencyCheck.blocked) {
|
|
620
|
+
recordExpansionDelegationTelemetry({
|
|
621
|
+
deps: input.deps,
|
|
622
|
+
component: "lcm_expand_query",
|
|
623
|
+
event: "block",
|
|
624
|
+
requestId,
|
|
625
|
+
sessionKey: callerSessionKey,
|
|
626
|
+
expansionDepth: recursionCheck.expansionDepth,
|
|
627
|
+
originSessionKey: concurrencyCheck.originSessionKey,
|
|
628
|
+
reason: concurrencyCheck.reason,
|
|
629
|
+
});
|
|
630
|
+
return jsonResult({
|
|
631
|
+
errorCode: concurrencyCheck.code,
|
|
632
|
+
error: concurrencyCheck.message,
|
|
633
|
+
requestId: concurrencyCheck.requestId,
|
|
634
|
+
expansionDepth: recursionCheck.expansionDepth,
|
|
635
|
+
originSessionKey: concurrencyCheck.originSessionKey,
|
|
636
|
+
reason: concurrencyCheck.reason,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
611
640
|
const requesterAgentId = input.deps.normalizeAgentId(
|
|
612
641
|
input.deps.parseAgentSessionKey(callerSessionKey)?.agentId,
|
|
613
642
|
);
|
|
614
643
|
const childExpansionDepth = resolveNextExpansionDepth(callerSessionKey);
|
|
615
|
-
const originSessionKey = recursionCheck.originSessionKey || callerSessionKey || "main";
|
|
616
644
|
|
|
617
645
|
const task = buildDelegatedExpandQueryTask({
|
|
618
646
|
summaryIds,
|
|
@@ -787,6 +815,11 @@ export function createLcmExpandQueryTool(input: {
|
|
|
787
815
|
return jsonResult({
|
|
788
816
|
error: failure,
|
|
789
817
|
});
|
|
818
|
+
} finally {
|
|
819
|
+
releaseExpansionConcurrencySlot({
|
|
820
|
+
originSessionKey,
|
|
821
|
+
requestId,
|
|
822
|
+
});
|
|
790
823
|
}
|
|
791
824
|
},
|
|
792
825
|
};
|
|
@@ -3,6 +3,7 @@ import { resolveDelegatedExpansionGrantId } from "../expansion-auth.js";
|
|
|
3
3
|
import type { LcmDependencies } from "../types.js";
|
|
4
4
|
|
|
5
5
|
export const EXPANSION_RECURSION_ERROR_CODE = "EXPANSION_RECURSION_BLOCKED";
|
|
6
|
+
export const EXPANSION_CONCURRENCY_ERROR_CODE = "EXPANSION_CONCURRENCY_BLOCKED";
|
|
6
7
|
const EXPANSION_DELEGATION_DEPTH_CAP = 1;
|
|
7
8
|
|
|
8
9
|
type TelemetryEvent = "start" | "block" | "timeout" | "success";
|
|
@@ -23,6 +24,7 @@ export type DelegatedExpansionContext = {
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export type ExpansionRecursionBlockReason = "depth_cap" | "idempotent_reentry";
|
|
27
|
+
export type ExpansionConcurrencyBlockReason = "origin_session_in_flight";
|
|
26
28
|
|
|
27
29
|
export type ExpansionRecursionGuardDecision =
|
|
28
30
|
| {
|
|
@@ -41,8 +43,24 @@ export type ExpansionRecursionGuardDecision =
|
|
|
41
43
|
originSessionKey: string;
|
|
42
44
|
};
|
|
43
45
|
|
|
46
|
+
export type ExpansionConcurrencyGuardDecision =
|
|
47
|
+
| {
|
|
48
|
+
blocked: false;
|
|
49
|
+
requestId: string;
|
|
50
|
+
originSessionKey: string;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
blocked: true;
|
|
54
|
+
code: typeof EXPANSION_CONCURRENCY_ERROR_CODE;
|
|
55
|
+
reason: ExpansionConcurrencyBlockReason;
|
|
56
|
+
message: string;
|
|
57
|
+
requestId: string;
|
|
58
|
+
originSessionKey: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
44
61
|
const delegatedContextBySessionKey = new Map<string, DelegatedExpansionContext>();
|
|
45
62
|
const blockedRequestIdsBySessionKey = new Map<string, Set<string>>();
|
|
63
|
+
const activeRequestIdByOriginSessionKey = new Map<string, string>();
|
|
46
64
|
|
|
47
65
|
function normalizeSessionKey(sessionKey?: string): string {
|
|
48
66
|
return typeof sessionKey === "string" ? sessionKey.trim() : "";
|
|
@@ -90,6 +108,14 @@ function buildExpansionRecursionRecoveryGuidance(originSessionKey: string): stri
|
|
|
90
108
|
);
|
|
91
109
|
}
|
|
92
110
|
|
|
111
|
+
function buildExpansionConcurrencyRecoveryGuidance(originSessionKey: string): string {
|
|
112
|
+
return (
|
|
113
|
+
"Recovery: Wait for the active expansion to finish before retrying. " +
|
|
114
|
+
`If you need an immediate fallback, stay in the origin session (${originSessionKey}) ` +
|
|
115
|
+
"and use `lcm_grep` or `lcm_describe` instead."
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
93
119
|
/**
|
|
94
120
|
* Create a stable request identifier for delegated expansion orchestration.
|
|
95
121
|
*/
|
|
@@ -213,6 +239,66 @@ export function evaluateExpansionRecursionGuard(params: {
|
|
|
213
239
|
};
|
|
214
240
|
}
|
|
215
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Acquire the single active delegated-expansion slot for an origin session.
|
|
244
|
+
* A second concurrent caller from the same origin session is blocked
|
|
245
|
+
* immediately so sub-agents do not deadlock on a shared lane.
|
|
246
|
+
*/
|
|
247
|
+
export function acquireExpansionConcurrencySlot(params: {
|
|
248
|
+
originSessionKey?: string;
|
|
249
|
+
requestId: string;
|
|
250
|
+
}): ExpansionConcurrencyGuardDecision {
|
|
251
|
+
const originSessionKey = normalizeSessionKey(params.originSessionKey) || "main";
|
|
252
|
+
const requestId = params.requestId.trim();
|
|
253
|
+
const activeRequestId = activeRequestIdByOriginSessionKey.get(originSessionKey);
|
|
254
|
+
|
|
255
|
+
if (activeRequestId && activeRequestId !== requestId) {
|
|
256
|
+
return {
|
|
257
|
+
blocked: true,
|
|
258
|
+
code: EXPANSION_CONCURRENCY_ERROR_CODE,
|
|
259
|
+
reason: "origin_session_in_flight",
|
|
260
|
+
message:
|
|
261
|
+
`${EXPANSION_CONCURRENCY_ERROR_CODE}: Another lcm_expand_query delegation is already ` +
|
|
262
|
+
`in flight for origin session (${originSessionKey}; activeRequestId=${activeRequestId}). ` +
|
|
263
|
+
buildExpansionConcurrencyRecoveryGuidance(originSessionKey),
|
|
264
|
+
requestId,
|
|
265
|
+
originSessionKey,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!activeRequestId) {
|
|
270
|
+
activeRequestIdByOriginSessionKey.set(originSessionKey, requestId);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
blocked: false,
|
|
275
|
+
requestId,
|
|
276
|
+
originSessionKey,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Release the active delegated-expansion slot for an origin session.
|
|
282
|
+
*/
|
|
283
|
+
export function releaseExpansionConcurrencySlot(params: {
|
|
284
|
+
originSessionKey?: string;
|
|
285
|
+
requestId?: string;
|
|
286
|
+
}): void {
|
|
287
|
+
const originSessionKey = normalizeSessionKey(params.originSessionKey);
|
|
288
|
+
if (!originSessionKey) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const activeRequestId = activeRequestIdByOriginSessionKey.get(originSessionKey);
|
|
292
|
+
if (!activeRequestId) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const requestId = params.requestId?.trim();
|
|
296
|
+
if (requestId && activeRequestId !== requestId) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
activeRequestIdByOriginSessionKey.delete(originSessionKey);
|
|
300
|
+
}
|
|
301
|
+
|
|
216
302
|
/**
|
|
217
303
|
* Emit structured delegated expansion telemetry with monotonic counters.
|
|
218
304
|
*/
|
|
@@ -279,6 +365,7 @@ export function getExpansionDelegationTelemetrySnapshotForTests(): Record<Teleme
|
|
|
279
365
|
export function resetExpansionDelegationGuardForTests(): void {
|
|
280
366
|
delegatedContextBySessionKey.clear();
|
|
281
367
|
blockedRequestIdsBySessionKey.clear();
|
|
368
|
+
activeRequestIdByOriginSessionKey.clear();
|
|
282
369
|
telemetryCounters.start = 0;
|
|
283
370
|
telemetryCounters.block = 0;
|
|
284
371
|
telemetryCounters.timeout = 0;
|