@martian-engineering/lossless-claw 0.5.1 → 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.
@@ -21,7 +21,7 @@ import {
21
21
  stampDelegatedExpansionContext,
22
22
  } from "./lcm-expansion-recursion-guard.js";
23
23
 
24
- const DELEGATED_WAIT_TIMEOUT_MS = 120_000;
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,16 +75,98 @@ 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
 
94
+ function collectExpansionFailureText(value: unknown, parts: string[], depth = 0): void {
95
+ if (depth > 3 || value == null) {
96
+ return;
97
+ }
98
+ if (typeof value === "string") {
99
+ const trimmed = value.trim();
100
+ if (trimmed) {
101
+ parts.push(trimmed);
102
+ }
103
+ return;
104
+ }
105
+ if (typeof value === "number" || typeof value === "boolean") {
106
+ parts.push(String(value));
107
+ return;
108
+ }
109
+ if (value instanceof Error) {
110
+ if (value.message.trim()) {
111
+ parts.push(value.message.trim());
112
+ }
113
+ collectExpansionFailureText(value.cause, parts, depth + 1);
114
+ return;
115
+ }
116
+ if (Array.isArray(value)) {
117
+ for (const entry of value) {
118
+ collectExpansionFailureText(entry, parts, depth + 1);
119
+ }
120
+ return;
121
+ }
122
+ if (typeof value === "object") {
123
+ const record = value as Record<string, unknown>;
124
+ for (const key of ["message", "error", "reason", "details", "response", "cause", "code"]) {
125
+ collectExpansionFailureText(record[key], parts, depth + 1);
126
+ }
127
+ }
128
+ }
129
+
130
+ function formatExpansionFailure(error: unknown): string {
131
+ const parts: string[] = [];
132
+ collectExpansionFailureText(error, parts);
133
+ const message = parts.join(" ").replace(/\s+/g, " ").trim();
134
+ if (message) {
135
+ return message;
136
+ }
137
+ if (typeof error === "string" && error.trim()) {
138
+ return error.trim();
139
+ }
140
+ return "Delegated expansion query failed.";
141
+ }
142
+
143
+ function shouldRetryWithoutOverride(message: string): boolean {
144
+ const normalized = message.toLowerCase();
145
+ return [
146
+ "model.request",
147
+ "missing scopes",
148
+ "insufficient scope",
149
+ "unauthorized",
150
+ "not authorized",
151
+ "forbidden",
152
+ "provider/model overrides are not authorized",
153
+ "model override is not authorized",
154
+ "unknown model",
155
+ "model not found",
156
+ "invalid model",
157
+ "not available",
158
+ "not supported",
159
+ "401",
160
+ "403",
161
+ ].some((signal) => normalized.includes(signal));
162
+ }
163
+
83
164
  /**
84
165
  * Build the sub-agent task message for delegated expansion and prompt answering.
85
166
  */
86
167
  function buildDelegatedExpandQueryTask(params: {
87
168
  summaryIds: string[];
169
+ messageBackedSummaryIds: string[];
88
170
  conversationId: number;
89
171
  query?: string;
90
172
  prompt: string;
@@ -95,6 +177,10 @@ function buildDelegatedExpandQueryTask(params: {
95
177
  originSessionKey: string;
96
178
  }) {
97
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)";
98
184
  return [
99
185
  "You are an autonomous LCM retrieval navigator. Plan and execute retrieval before answering.",
100
186
  "",
@@ -102,6 +188,7 @@ function buildDelegatedExpandQueryTask(params: {
102
188
  `Conversation scope: ${params.conversationId}`,
103
189
  `Expansion token budget (total across this run): ${params.tokenCap}`,
104
190
  `Seed summary IDs: ${seedSummaryIds}`,
191
+ `Seed summaries requiring raw message expansion: ${messageBackedSummaryIds}`,
105
192
  params.query ? `Routing query: ${params.query}` : undefined,
106
193
  "",
107
194
  "Strategy:",
@@ -109,7 +196,7 @@ function buildDelegatedExpandQueryTask(params: {
109
196
  "2. If additional candidates are needed, use `lcm_grep` scoped to summaries.",
110
197
  "3. Select branches that fit remaining budget; prefer high-signal paths first.",
111
198
  "4. Call `lcm_expand` selectively (do not expand everything blindly).",
112
- "5. Keep includeMessages=false by default; use includeMessages=true only for specific leaf evidence.",
199
+ "5. Keep includeMessages=false by default; use includeMessages=true for the message-backed seed summaries above and any other specific leaf evidence.",
113
200
  `6. Stay within ${params.tokenCap} total expansion tokens across all lcm_expand calls.`,
114
201
  "",
115
202
  "User prompt to answer:",
@@ -141,24 +228,25 @@ function buildDelegatedExpandQueryTask(params: {
141
228
  ].join("\n");
142
229
  }
143
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
+
144
237
  /**
145
- * 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.
146
239
  */
147
240
  function parseDelegatedExpandQueryReply(
148
241
  rawReply: string | undefined,
149
242
  fallbackExpandedSummaryCount: number,
150
- ): ExpandQueryReply {
151
- const fallback: ExpandQueryReply = {
152
- answer: (rawReply ?? "").trim(),
153
- citedIds: [],
154
- expandedSummaryCount: fallbackExpandedSummaryCount,
155
- totalSourceTokens: 0,
156
- truncated: false,
157
- };
158
-
243
+ ): ParsedExpandQueryReply {
159
244
  const reply = rawReply?.trim();
160
245
  if (!reply) {
161
- return fallback;
246
+ return {
247
+ ok: false,
248
+ error: "Delegated expansion query returned an empty reply.",
249
+ };
162
250
  }
163
251
 
164
252
  const candidates: string[] = [reply];
@@ -177,6 +265,12 @@ function parseDelegatedExpandQueryReply(
177
265
  truncated?: unknown;
178
266
  };
179
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
+ }
180
274
  const citedIds = normalizeSummaryIds(
181
275
  Array.isArray(parsed.citedIds)
182
276
  ? parsed.citedIds.filter((value): value is string => typeof value === "string")
@@ -194,18 +288,24 @@ function parseDelegatedExpandQueryReply(
194
288
  const truncated = parsed.truncated === true;
195
289
 
196
290
  return {
197
- answer: answer || fallback.answer,
198
- citedIds,
199
- expandedSummaryCount,
200
- totalSourceTokens,
201
- truncated,
291
+ ok: true,
292
+ value: {
293
+ answer,
294
+ citedIds,
295
+ expandedSummaryCount,
296
+ totalSourceTokens,
297
+ truncated,
298
+ },
202
299
  };
203
300
  } catch {
204
301
  // Try next candidate.
205
302
  }
206
303
  }
207
304
 
208
- return fallback;
305
+ return {
306
+ ok: false,
307
+ error: formatInvalidDelegatedReply(reply, "non-JSON output"),
308
+ };
209
309
  }
210
310
 
211
311
  /**
@@ -246,6 +346,22 @@ function resolveSourceConversationId(params: {
246
346
  );
247
347
  }
248
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
+
249
365
  /**
250
366
  * Resolve summary candidates from explicit IDs and/or query matches.
251
367
  */
@@ -263,13 +379,15 @@ async function resolveSummaryCandidates(params: {
263
379
  if (!described || described.type !== "summary" || !described.summary) {
264
380
  throw new Error(`Summary not found: ${summaryId}`);
265
381
  }
266
- candidates.set(summaryId, {
382
+ upsertSummaryCandidate(candidates, {
267
383
  summaryId,
268
384
  conversationId: described.summary.conversationId,
385
+ requiresMessageExpansion: false,
269
386
  });
270
387
  }
271
388
 
272
389
  if (params.query) {
390
+ const summaryStore = params.lcm.getSummaryStore();
273
391
  const grepResult = await retrieval.grep({
274
392
  query: params.query,
275
393
  mode: "full_text",
@@ -277,11 +395,46 @@ async function resolveSummaryCandidates(params: {
277
395
  conversationId: params.conversationId,
278
396
  });
279
397
  for (const summary of grepResult.summaries) {
280
- candidates.set(summary.summaryId, {
398
+ upsertSummaryCandidate(candidates, {
281
399
  summaryId: summary.summaryId,
282
400
  conversationId: summary.conversationId,
401
+ requiresMessageExpansion: false,
283
402
  });
284
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
+ }
285
438
  }
286
439
 
287
440
  return Array.from(candidates.values());
@@ -297,6 +450,10 @@ export function createLcmExpandQueryTool(input: {
297
450
  /** Session key for scope fallback when sessionId is unavailable. */
298
451
  sessionKey?: string;
299
452
  }): AnyAgentTool {
453
+ const delegatedWaitTimeoutMs =
454
+ input.deps.config.delegationTimeoutMs || DEFAULT_DELEGATED_WAIT_TIMEOUT_MS;
455
+ const delegatedWaitTimeoutSeconds = Math.ceil(delegatedWaitTimeoutMs / 1000);
456
+
300
457
  return {
301
458
  name: "lcm_expand_query",
302
459
  label: "LCM Expand Query",
@@ -401,9 +558,6 @@ export function createLcmExpandQueryTool(input: {
401
558
  });
402
559
  }
403
560
 
404
- let childSessionKey = "";
405
- let grantCreated = false;
406
-
407
561
  try {
408
562
  const candidates = await resolveSummaryCandidates({
409
563
  lcm: input.lcm,
@@ -438,6 +592,15 @@ export function createLcmExpandQueryTool(input: {
438
592
  .filter((candidate) => candidate.conversationId === sourceConversationId)
439
593
  .map((candidate) => candidate.summaryId),
440
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
+ );
441
604
 
442
605
  if (summaryIds.length === 0) {
443
606
  return jsonResult({
@@ -448,28 +611,12 @@ export function createLcmExpandQueryTool(input: {
448
611
  const requesterAgentId = input.deps.normalizeAgentId(
449
612
  input.deps.parseAgentSessionKey(callerSessionKey)?.agentId,
450
613
  );
451
- childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
452
614
  const childExpansionDepth = resolveNextExpansionDepth(callerSessionKey);
453
615
  const originSessionKey = recursionCheck.originSessionKey || callerSessionKey || "main";
454
616
 
455
- createDelegatedExpansionGrant({
456
- delegatedSessionKey: childSessionKey,
457
- issuerSessionId: callerSessionKey || "main",
458
- allowedConversationIds: [sourceConversationId],
459
- tokenCap: expansionTokenCap,
460
- ttlMs: DELEGATED_WAIT_TIMEOUT_MS + 30_000,
461
- });
462
- stampDelegatedExpansionContext({
463
- sessionKey: childSessionKey,
464
- requestId,
465
- expansionDepth: childExpansionDepth,
466
- originSessionKey,
467
- stampedBy: "lcm_expand_query",
468
- });
469
- grantCreated = true;
470
-
471
617
  const task = buildDelegatedExpandQueryTask({
472
618
  summaryIds,
619
+ messageBackedSummaryIds,
473
620
  conversationId: sourceConversationId,
474
621
  query: query || undefined,
475
622
  prompt,
@@ -480,118 +627,166 @@ export function createLcmExpandQueryTool(input: {
480
627
  originSessionKey,
481
628
  });
482
629
 
483
- const childIdem = crypto.randomUUID();
484
630
  const expansionProvider = input.deps.config.expansionProvider || undefined;
485
631
  const expansionModel = input.deps.config.expansionModel || undefined;
486
- const response = (await input.deps.callGateway({
487
- method: "agent",
488
- params: {
489
- message: task,
490
- sessionKey: childSessionKey,
491
- deliver: false,
492
- lane: input.deps.agentLaneSubagent,
493
- idempotencyKey: childIdem,
494
- ...(expansionProvider ? { provider: expansionProvider } : {}),
495
- ...(expansionModel ? { model: expansionModel } : {}),
496
- extraSystemPrompt: input.deps.buildSubagentSystemPrompt({
497
- depth: 1,
498
- maxDepth: 8,
499
- taskSummary: "Run lcm_expand and return prompt-focused JSON answer",
500
- }),
501
- },
502
- timeoutMs: GATEWAY_TIMEOUT_MS,
503
- })) as { runId?: string };
504
-
505
- const runId = typeof response?.runId === "string" ? response.runId.trim() : "";
506
- if (!runId) {
507
- return jsonResult({
508
- error: "Delegated expansion did not return a runId.",
509
- });
510
- }
511
-
512
- const wait = (await input.deps.callGateway({
513
- method: "agent.wait",
514
- params: {
515
- runId,
516
- timeoutMs: DELEGATED_WAIT_TIMEOUT_MS,
517
- },
518
- timeoutMs: DELEGATED_WAIT_TIMEOUT_MS,
519
- })) as { status?: string; error?: string };
520
- const status = typeof wait?.status === "string" ? wait.status : "error";
521
- if (status === "timeout") {
522
- recordExpansionDelegationTelemetry({
523
- deps: input.deps,
524
- component: "lcm_expand_query",
525
- event: "timeout",
526
- requestId,
527
- sessionKey: callerSessionKey,
528
- expansionDepth: childExpansionDepth,
529
- originSessionKey,
530
- runId,
531
- });
532
- return jsonResult({
533
- error: "lcm_expand_query timed out waiting for delegated expansion (120s).",
534
- });
535
- }
536
- if (status !== "ok") {
537
- return jsonResult({
538
- error:
539
- typeof wait?.error === "string" && wait.error.trim()
540
- ? wait.error
541
- : "Delegated expansion query failed.",
542
- });
543
- }
544
-
545
- const replyPayload = (await input.deps.callGateway({
546
- method: "sessions.get",
547
- params: { key: childSessionKey, limit: 80 },
548
- timeoutMs: GATEWAY_TIMEOUT_MS,
549
- })) as { messages?: unknown[] };
550
- const reply = input.deps.readLatestAssistantReply(
551
- Array.isArray(replyPayload.messages) ? replyPayload.messages : [],
552
- );
553
- const parsed = parseDelegatedExpandQueryReply(reply, summaryIds.length);
554
- recordExpansionDelegationTelemetry({
555
- deps: input.deps,
556
- component: "lcm_expand_query",
557
- event: "success",
558
- requestId,
559
- sessionKey: callerSessionKey,
560
- expansionDepth: childExpansionDepth,
561
- originSessionKey,
562
- runId,
563
- });
632
+ const canonicalExpansionModel = expansionModel?.includes("/") ? expansionModel : undefined;
633
+ const delegatedOverrideProvider = canonicalExpansionModel ? undefined : expansionProvider;
634
+ const delegatedOverrideModel = canonicalExpansionModel || expansionModel;
635
+ const configuredOverrideLabel =
636
+ delegatedOverrideProvider && delegatedOverrideModel
637
+ ? `${delegatedOverrideProvider}/${delegatedOverrideModel}`
638
+ : delegatedOverrideModel || delegatedOverrideProvider || "configured override";
639
+
640
+ const runDelegatedQuery = async (provider?: string, model?: string) => {
641
+ const childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
642
+ const childIdem = crypto.randomUUID();
643
+ let grantCreated = false;
564
644
 
565
- return jsonResult({
566
- answer: parsed.answer,
567
- citedIds: parsed.citedIds,
568
- sourceConversationId,
569
- expandedSummaryCount: parsed.expandedSummaryCount,
570
- totalSourceTokens: parsed.totalSourceTokens,
571
- truncated: parsed.truncated,
572
- });
573
- } catch (error) {
574
- return jsonResult({
575
- error: error instanceof Error ? error.message : String(error),
576
- });
577
- } finally {
578
- if (childSessionKey) {
579
645
  try {
580
- await input.deps.callGateway({
581
- method: "sessions.delete",
582
- params: { key: childSessionKey, deleteTranscript: true },
646
+ createDelegatedExpansionGrant({
647
+ delegatedSessionKey: childSessionKey,
648
+ issuerSessionId: callerSessionKey || "main",
649
+ allowedConversationIds: [sourceConversationId],
650
+ tokenCap: expansionTokenCap,
651
+ ttlMs: delegatedWaitTimeoutMs + 30_000,
652
+ });
653
+ stampDelegatedExpansionContext({
654
+ sessionKey: childSessionKey,
655
+ requestId,
656
+ expansionDepth: childExpansionDepth,
657
+ originSessionKey,
658
+ stampedBy: "lcm_expand_query",
659
+ });
660
+ grantCreated = true;
661
+
662
+ const response = (await input.deps.callGateway({
663
+ method: "agent",
664
+ params: {
665
+ message: task,
666
+ sessionKey: childSessionKey,
667
+ deliver: false,
668
+ lane: input.deps.agentLaneSubagent,
669
+ idempotencyKey: childIdem,
670
+ ...(provider ? { provider } : {}),
671
+ ...(model ? { model } : {}),
672
+ extraSystemPrompt: input.deps.buildSubagentSystemPrompt({
673
+ depth: 1,
674
+ maxDepth: 8,
675
+ taskSummary: "Run lcm_expand and return prompt-focused JSON answer",
676
+ }),
677
+ },
583
678
  timeoutMs: GATEWAY_TIMEOUT_MS,
679
+ })) as { runId?: unknown; error?: unknown };
680
+
681
+ const runId = typeof response?.runId === "string" ? response.runId.trim() : "";
682
+ if (!runId) {
683
+ throw new Error(
684
+ formatExpansionFailure(response?.error ?? response)
685
+ || "Delegated expansion did not return a runId.",
686
+ );
687
+ }
688
+
689
+ const wait = (await input.deps.callGateway({
690
+ method: "agent.wait",
691
+ params: {
692
+ runId,
693
+ timeoutMs: delegatedWaitTimeoutMs,
694
+ },
695
+ timeoutMs: delegatedWaitTimeoutMs,
696
+ })) as { status?: string; error?: unknown };
697
+ const status = typeof wait?.status === "string" ? wait.status : "error";
698
+ if (status === "timeout") {
699
+ recordExpansionDelegationTelemetry({
700
+ deps: input.deps,
701
+ component: "lcm_expand_query",
702
+ event: "timeout",
703
+ requestId,
704
+ sessionKey: callerSessionKey,
705
+ expansionDepth: childExpansionDepth,
706
+ originSessionKey,
707
+ runId,
708
+ });
709
+ throw new Error(
710
+ `lcm_expand_query timed out waiting for delegated expansion (${delegatedWaitTimeoutSeconds}s).`,
711
+ );
712
+ }
713
+ if (status !== "ok") {
714
+ throw new Error(formatExpansionFailure(wait?.error));
715
+ }
716
+
717
+ const replyPayload = (await input.deps.callGateway({
718
+ method: "sessions.get",
719
+ params: { key: childSessionKey, limit: 80 },
720
+ timeoutMs: GATEWAY_TIMEOUT_MS,
721
+ })) as { messages?: unknown[] };
722
+ const reply = input.deps.readLatestAssistantReply(
723
+ Array.isArray(replyPayload.messages) ? replyPayload.messages : [],
724
+ );
725
+ const parsed = parseDelegatedExpandQueryReply(reply, summaryIds.length);
726
+ if (!parsed.ok) {
727
+ throw new Error(parsed.error);
728
+ }
729
+ recordExpansionDelegationTelemetry({
730
+ deps: input.deps,
731
+ component: "lcm_expand_query",
732
+ event: "success",
733
+ requestId,
734
+ sessionKey: callerSessionKey,
735
+ expansionDepth: childExpansionDepth,
736
+ originSessionKey,
737
+ runId,
738
+ });
739
+
740
+ return jsonResult({
741
+ answer: parsed.value.answer,
742
+ citedIds: parsed.value.citedIds,
743
+ sourceConversationId,
744
+ expandedSummaryCount: parsed.value.expandedSummaryCount,
745
+ totalSourceTokens: parsed.value.totalSourceTokens,
746
+ truncated: parsed.value.truncated,
584
747
  });
585
- } catch {
586
- // Cleanup is best-effort.
748
+ } finally {
749
+ try {
750
+ await input.deps.callGateway({
751
+ method: "sessions.delete",
752
+ params: { key: childSessionKey, deleteTranscript: true },
753
+ timeoutMs: GATEWAY_TIMEOUT_MS,
754
+ });
755
+ } catch {
756
+ // Cleanup is best-effort.
757
+ }
758
+ if (grantCreated) {
759
+ revokeDelegatedExpansionGrantForSession(childSessionKey, { removeBinding: true });
760
+ }
761
+ clearDelegatedExpansionContext(childSessionKey);
587
762
  }
763
+ };
764
+
765
+ if (!expansionProvider && !expansionModel) {
766
+ return await runDelegatedQuery();
588
767
  }
589
- if (grantCreated && childSessionKey) {
590
- revokeDelegatedExpansionGrantForSession(childSessionKey, { removeBinding: true });
591
- }
592
- if (childSessionKey) {
593
- clearDelegatedExpansionContext(childSessionKey);
768
+
769
+ try {
770
+ return await runDelegatedQuery(delegatedOverrideProvider, delegatedOverrideModel);
771
+ } catch (error) {
772
+ const failure = formatExpansionFailure(error);
773
+ input.deps.log.warn(
774
+ `[lcm] delegated expansion override failed (${configuredOverrideLabel}): ${failure}`,
775
+ );
776
+ if (!shouldRetryWithoutOverride(failure)) {
777
+ throw new Error(failure);
778
+ }
779
+ input.deps.log.warn(
780
+ `[lcm] retrying delegated expansion without provider/model override after: ${failure}`,
781
+ );
782
+ return await runDelegatedQuery();
594
783
  }
784
+ } catch (error) {
785
+ const failure = formatExpansionFailure(error);
786
+ input.deps.log.error(`[lcm] delegated expansion query failed: ${failure}`);
787
+ return jsonResult({
788
+ error: failure,
789
+ });
595
790
  }
596
791
  },
597
792
  };
package/src/types.ts CHANGED
@@ -72,6 +72,7 @@ export type ApiKeyLookupOptions = {
72
72
  preferredProfile?: string;
73
73
  agentDir?: string;
74
74
  runtimeConfig?: unknown;
75
+ skipModelAuth?: boolean;
75
76
  };
76
77
 
77
78
  export type GetApiKeyFn = (