@martian-engineering/lossless-claw 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,15 +13,17 @@ 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,
22
24
  } from "./lcm-expansion-recursion-guard.js";
23
25
 
24
- const DELEGATED_WAIT_TIMEOUT_MS = 120_000;
26
+ const DEFAULT_DELEGATED_WAIT_TIMEOUT_MS = 120_000;
25
27
  const GATEWAY_TIMEOUT_MS = 10_000;
26
28
  const DEFAULT_MAX_ANSWER_TOKENS = 2_000;
27
29
 
@@ -75,9 +77,20 @@ type ExpandQueryReply = {
75
77
  truncated: boolean;
76
78
  };
77
79
 
80
+ type ParsedExpandQueryReply =
81
+ | {
82
+ ok: true;
83
+ value: ExpandQueryReply;
84
+ }
85
+ | {
86
+ ok: false;
87
+ error: string;
88
+ };
89
+
78
90
  type SummaryCandidate = {
79
91
  summaryId: string;
80
92
  conversationId: number;
93
+ requiresMessageExpansion: boolean;
81
94
  };
82
95
 
83
96
  function collectExpansionFailureText(value: unknown, parts: string[], depth = 0): void {
@@ -155,6 +168,7 @@ function shouldRetryWithoutOverride(message: string): boolean {
155
168
  */
156
169
  function buildDelegatedExpandQueryTask(params: {
157
170
  summaryIds: string[];
171
+ messageBackedSummaryIds: string[];
158
172
  conversationId: number;
159
173
  query?: string;
160
174
  prompt: string;
@@ -165,6 +179,10 @@ function buildDelegatedExpandQueryTask(params: {
165
179
  originSessionKey: string;
166
180
  }) {
167
181
  const seedSummaryIds = params.summaryIds.length > 0 ? params.summaryIds.join(", ") : "(none)";
182
+ const messageBackedSummaryIds =
183
+ params.messageBackedSummaryIds.length > 0
184
+ ? params.messageBackedSummaryIds.join(", ")
185
+ : "(none)";
168
186
  return [
169
187
  "You are an autonomous LCM retrieval navigator. Plan and execute retrieval before answering.",
170
188
  "",
@@ -172,6 +190,7 @@ function buildDelegatedExpandQueryTask(params: {
172
190
  `Conversation scope: ${params.conversationId}`,
173
191
  `Expansion token budget (total across this run): ${params.tokenCap}`,
174
192
  `Seed summary IDs: ${seedSummaryIds}`,
193
+ `Seed summaries requiring raw message expansion: ${messageBackedSummaryIds}`,
175
194
  params.query ? `Routing query: ${params.query}` : undefined,
176
195
  "",
177
196
  "Strategy:",
@@ -179,7 +198,7 @@ function buildDelegatedExpandQueryTask(params: {
179
198
  "2. If additional candidates are needed, use `lcm_grep` scoped to summaries.",
180
199
  "3. Select branches that fit remaining budget; prefer high-signal paths first.",
181
200
  "4. Call `lcm_expand` selectively (do not expand everything blindly).",
182
- "5. Keep includeMessages=false by default; use includeMessages=true only for specific leaf evidence.",
201
+ "5. Keep includeMessages=false by default; use includeMessages=true for the message-backed seed summaries above and any other specific leaf evidence.",
183
202
  `6. Stay within ${params.tokenCap} total expansion tokens across all lcm_expand calls.`,
184
203
  "",
185
204
  "User prompt to answer:",
@@ -211,24 +230,25 @@ function buildDelegatedExpandQueryTask(params: {
211
230
  ].join("\n");
212
231
  }
213
232
 
233
+ function formatInvalidDelegatedReply(reply: string, reason: string): string {
234
+ const compact = reply.replace(/\s+/g, " ").trim();
235
+ const snippet = compact.length <= 240 ? compact : `${compact.slice(0, 240)}...`;
236
+ return `Delegated expansion query returned ${reason}: ${snippet}`;
237
+ }
238
+
214
239
  /**
215
- * Parse the child reply; accepts plain JSON or fenced JSON.
240
+ * Parse the child reply; accepts plain JSON or fenced JSON and rejects malformed fallbacks.
216
241
  */
217
242
  function parseDelegatedExpandQueryReply(
218
243
  rawReply: string | undefined,
219
244
  fallbackExpandedSummaryCount: number,
220
- ): ExpandQueryReply {
221
- const fallback: ExpandQueryReply = {
222
- answer: (rawReply ?? "").trim(),
223
- citedIds: [],
224
- expandedSummaryCount: fallbackExpandedSummaryCount,
225
- totalSourceTokens: 0,
226
- truncated: false,
227
- };
228
-
245
+ ): ParsedExpandQueryReply {
229
246
  const reply = rawReply?.trim();
230
247
  if (!reply) {
231
- return fallback;
248
+ return {
249
+ ok: false,
250
+ error: "Delegated expansion query returned an empty reply.",
251
+ };
232
252
  }
233
253
 
234
254
  const candidates: string[] = [reply];
@@ -247,6 +267,12 @@ function parseDelegatedExpandQueryReply(
247
267
  truncated?: unknown;
248
268
  };
249
269
  const answer = typeof parsed.answer === "string" ? parsed.answer.trim() : "";
270
+ if (!answer) {
271
+ return {
272
+ ok: false,
273
+ error: formatInvalidDelegatedReply(reply, 'JSON without a non-empty "answer"'),
274
+ };
275
+ }
250
276
  const citedIds = normalizeSummaryIds(
251
277
  Array.isArray(parsed.citedIds)
252
278
  ? parsed.citedIds.filter((value): value is string => typeof value === "string")
@@ -264,18 +290,24 @@ function parseDelegatedExpandQueryReply(
264
290
  const truncated = parsed.truncated === true;
265
291
 
266
292
  return {
267
- answer: answer || fallback.answer,
268
- citedIds,
269
- expandedSummaryCount,
270
- totalSourceTokens,
271
- truncated,
293
+ ok: true,
294
+ value: {
295
+ answer,
296
+ citedIds,
297
+ expandedSummaryCount,
298
+ totalSourceTokens,
299
+ truncated,
300
+ },
272
301
  };
273
302
  } catch {
274
303
  // Try next candidate.
275
304
  }
276
305
  }
277
306
 
278
- return fallback;
307
+ return {
308
+ ok: false,
309
+ error: formatInvalidDelegatedReply(reply, "non-JSON output"),
310
+ };
279
311
  }
280
312
 
281
313
  /**
@@ -316,6 +348,22 @@ function resolveSourceConversationId(params: {
316
348
  );
317
349
  }
318
350
 
351
+ function upsertSummaryCandidate(
352
+ candidates: Map<string, SummaryCandidate>,
353
+ candidate: SummaryCandidate,
354
+ ): void {
355
+ const existing = candidates.get(candidate.summaryId);
356
+ if (!existing) {
357
+ candidates.set(candidate.summaryId, candidate);
358
+ return;
359
+ }
360
+ candidates.set(candidate.summaryId, {
361
+ ...existing,
362
+ requiresMessageExpansion:
363
+ existing.requiresMessageExpansion || candidate.requiresMessageExpansion,
364
+ });
365
+ }
366
+
319
367
  /**
320
368
  * Resolve summary candidates from explicit IDs and/or query matches.
321
369
  */
@@ -333,13 +381,15 @@ async function resolveSummaryCandidates(params: {
333
381
  if (!described || described.type !== "summary" || !described.summary) {
334
382
  throw new Error(`Summary not found: ${summaryId}`);
335
383
  }
336
- candidates.set(summaryId, {
384
+ upsertSummaryCandidate(candidates, {
337
385
  summaryId,
338
386
  conversationId: described.summary.conversationId,
387
+ requiresMessageExpansion: false,
339
388
  });
340
389
  }
341
390
 
342
391
  if (params.query) {
392
+ const summaryStore = params.lcm.getSummaryStore();
343
393
  const grepResult = await retrieval.grep({
344
394
  query: params.query,
345
395
  mode: "full_text",
@@ -347,11 +397,46 @@ async function resolveSummaryCandidates(params: {
347
397
  conversationId: params.conversationId,
348
398
  });
349
399
  for (const summary of grepResult.summaries) {
350
- candidates.set(summary.summaryId, {
400
+ upsertSummaryCandidate(candidates, {
351
401
  summaryId: summary.summaryId,
352
402
  conversationId: summary.conversationId,
403
+ requiresMessageExpansion: false,
353
404
  });
354
405
  }
406
+
407
+ if (grepResult.summaries.length === 0 && typeof params.conversationId === "number") {
408
+ const maxDepth = await summaryStore.getConversationMaxSummaryDepth(params.conversationId);
409
+ if (typeof maxDepth === "number" && maxDepth <= 1) {
410
+ const messageResult = await retrieval.grep({
411
+ query: params.query,
412
+ mode: "full_text",
413
+ scope: "messages",
414
+ conversationId: params.conversationId,
415
+ });
416
+ const messageIds = messageResult.messages.map((message) => message.messageId);
417
+ const leafLinks = await summaryStore.getLeafSummaryLinksForMessageIds(
418
+ params.conversationId,
419
+ messageIds,
420
+ );
421
+ const summaryIdsByMessageId = new Map<number, string[]>();
422
+ for (const link of leafLinks) {
423
+ const linkedSummaryIds = summaryIdsByMessageId.get(link.messageId) ?? [];
424
+ if (!linkedSummaryIds.includes(link.summaryId)) {
425
+ linkedSummaryIds.push(link.summaryId);
426
+ summaryIdsByMessageId.set(link.messageId, linkedSummaryIds);
427
+ }
428
+ }
429
+ for (const message of messageResult.messages) {
430
+ for (const summaryId of summaryIdsByMessageId.get(message.messageId) ?? []) {
431
+ upsertSummaryCandidate(candidates, {
432
+ summaryId,
433
+ conversationId: params.conversationId,
434
+ requiresMessageExpansion: true,
435
+ });
436
+ }
437
+ }
438
+ }
439
+ }
355
440
  }
356
441
 
357
442
  return Array.from(candidates.values());
@@ -367,6 +452,10 @@ export function createLcmExpandQueryTool(input: {
367
452
  /** Session key for scope fallback when sessionId is unavailable. */
368
453
  sessionKey?: string;
369
454
  }): AnyAgentTool {
455
+ const delegatedWaitTimeoutMs =
456
+ input.deps.config.delegationTimeoutMs || DEFAULT_DELEGATED_WAIT_TIMEOUT_MS;
457
+ const delegatedWaitTimeoutSeconds = Math.ceil(delegatedWaitTimeoutMs / 1000);
458
+
370
459
  return {
371
460
  name: "lcm_expand_query",
372
461
  label: "LCM Expand Query",
@@ -444,34 +533,36 @@ export function createLcmExpandQueryTool(input: {
444
533
  });
445
534
  }
446
535
 
447
- const conversationScope = await resolveLcmConversationScope({
448
- lcm: input.lcm,
449
- deps: input.deps,
450
- sessionId: input.sessionId,
451
- sessionKey: input.sessionKey,
452
- params: p,
453
- });
454
- let scopedConversationId = conversationScope.conversationId;
455
- if (
456
- !conversationScope.allConversations &&
457
- scopedConversationId == null &&
458
- callerSessionKey
459
- ) {
460
- scopedConversationId = await resolveRequesterConversationScopeId({
461
- deps: input.deps,
462
- requesterSessionKey: callerSessionKey,
536
+ const originSessionKey = recursionCheck.originSessionKey || callerSessionKey || "main";
537
+
538
+ try {
539
+ const conversationScope = await resolveLcmConversationScope({
463
540
  lcm: input.lcm,
541
+ deps: input.deps,
542
+ sessionId: input.sessionId,
543
+ sessionKey: input.sessionKey,
544
+ params: p,
464
545
  });
465
- }
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
+ }
466
558
 
467
- if (!conversationScope.allConversations && scopedConversationId == null) {
468
- return jsonResult({
469
- error:
470
- "No LCM conversation found for this session. Provide conversationId or set allConversations=true.",
471
- });
472
- }
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
+ }
473
565
 
474
- try {
475
566
  const candidates = await resolveSummaryCandidates({
476
567
  lcm: input.lcm,
477
568
  explicitSummaryIds,
@@ -505,6 +596,15 @@ export function createLcmExpandQueryTool(input: {
505
596
  .filter((candidate) => candidate.conversationId === sourceConversationId)
506
597
  .map((candidate) => candidate.summaryId),
507
598
  );
599
+ const messageBackedSummaryIds = normalizeSummaryIds(
600
+ candidates
601
+ .filter(
602
+ (candidate) =>
603
+ candidate.conversationId === sourceConversationId &&
604
+ candidate.requiresMessageExpansion,
605
+ )
606
+ .map((candidate) => candidate.summaryId),
607
+ );
508
608
 
509
609
  if (summaryIds.length === 0) {
510
610
  return jsonResult({
@@ -512,14 +612,39 @@ export function createLcmExpandQueryTool(input: {
512
612
  });
513
613
  }
514
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
+
515
640
  const requesterAgentId = input.deps.normalizeAgentId(
516
641
  input.deps.parseAgentSessionKey(callerSessionKey)?.agentId,
517
642
  );
518
643
  const childExpansionDepth = resolveNextExpansionDepth(callerSessionKey);
519
- const originSessionKey = recursionCheck.originSessionKey || callerSessionKey || "main";
520
644
 
521
645
  const task = buildDelegatedExpandQueryTask({
522
646
  summaryIds,
647
+ messageBackedSummaryIds,
523
648
  conversationId: sourceConversationId,
524
649
  query: query || undefined,
525
650
  prompt,
@@ -532,10 +657,13 @@ export function createLcmExpandQueryTool(input: {
532
657
 
533
658
  const expansionProvider = input.deps.config.expansionProvider || undefined;
534
659
  const expansionModel = input.deps.config.expansionModel || undefined;
660
+ const canonicalExpansionModel = expansionModel?.includes("/") ? expansionModel : undefined;
661
+ const delegatedOverrideProvider = canonicalExpansionModel ? undefined : expansionProvider;
662
+ const delegatedOverrideModel = canonicalExpansionModel || expansionModel;
535
663
  const configuredOverrideLabel =
536
- expansionProvider && expansionModel
537
- ? `${expansionProvider}/${expansionModel}`
538
- : expansionModel || expansionProvider || "configured override";
664
+ delegatedOverrideProvider && delegatedOverrideModel
665
+ ? `${delegatedOverrideProvider}/${delegatedOverrideModel}`
666
+ : delegatedOverrideModel || delegatedOverrideProvider || "configured override";
539
667
 
540
668
  const runDelegatedQuery = async (provider?: string, model?: string) => {
541
669
  const childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
@@ -548,7 +676,7 @@ export function createLcmExpandQueryTool(input: {
548
676
  issuerSessionId: callerSessionKey || "main",
549
677
  allowedConversationIds: [sourceConversationId],
550
678
  tokenCap: expansionTokenCap,
551
- ttlMs: DELEGATED_WAIT_TIMEOUT_MS + 30_000,
679
+ ttlMs: delegatedWaitTimeoutMs + 30_000,
552
680
  });
553
681
  stampDelegatedExpansionContext({
554
682
  sessionKey: childSessionKey,
@@ -590,9 +718,9 @@ export function createLcmExpandQueryTool(input: {
590
718
  method: "agent.wait",
591
719
  params: {
592
720
  runId,
593
- timeoutMs: DELEGATED_WAIT_TIMEOUT_MS,
721
+ timeoutMs: delegatedWaitTimeoutMs,
594
722
  },
595
- timeoutMs: DELEGATED_WAIT_TIMEOUT_MS,
723
+ timeoutMs: delegatedWaitTimeoutMs,
596
724
  })) as { status?: string; error?: unknown };
597
725
  const status = typeof wait?.status === "string" ? wait.status : "error";
598
726
  if (status === "timeout") {
@@ -607,7 +735,7 @@ export function createLcmExpandQueryTool(input: {
607
735
  runId,
608
736
  });
609
737
  throw new Error(
610
- "lcm_expand_query timed out waiting for delegated expansion (120s).",
738
+ `lcm_expand_query timed out waiting for delegated expansion (${delegatedWaitTimeoutSeconds}s).`,
611
739
  );
612
740
  }
613
741
  if (status !== "ok") {
@@ -623,6 +751,9 @@ export function createLcmExpandQueryTool(input: {
623
751
  Array.isArray(replyPayload.messages) ? replyPayload.messages : [],
624
752
  );
625
753
  const parsed = parseDelegatedExpandQueryReply(reply, summaryIds.length);
754
+ if (!parsed.ok) {
755
+ throw new Error(parsed.error);
756
+ }
626
757
  recordExpansionDelegationTelemetry({
627
758
  deps: input.deps,
628
759
  component: "lcm_expand_query",
@@ -635,12 +766,12 @@ export function createLcmExpandQueryTool(input: {
635
766
  });
636
767
 
637
768
  return jsonResult({
638
- answer: parsed.answer,
639
- citedIds: parsed.citedIds,
769
+ answer: parsed.value.answer,
770
+ citedIds: parsed.value.citedIds,
640
771
  sourceConversationId,
641
- expandedSummaryCount: parsed.expandedSummaryCount,
642
- totalSourceTokens: parsed.totalSourceTokens,
643
- truncated: parsed.truncated,
772
+ expandedSummaryCount: parsed.value.expandedSummaryCount,
773
+ totalSourceTokens: parsed.value.totalSourceTokens,
774
+ truncated: parsed.value.truncated,
644
775
  });
645
776
  } finally {
646
777
  try {
@@ -664,7 +795,7 @@ export function createLcmExpandQueryTool(input: {
664
795
  }
665
796
 
666
797
  try {
667
- return await runDelegatedQuery(expansionProvider, expansionModel);
798
+ return await runDelegatedQuery(delegatedOverrideProvider, delegatedOverrideModel);
668
799
  } catch (error) {
669
800
  const failure = formatExpansionFailure(error);
670
801
  input.deps.log.warn(
@@ -684,6 +815,11 @@ export function createLcmExpandQueryTool(input: {
684
815
  return jsonResult({
685
816
  error: failure,
686
817
  });
818
+ } finally {
819
+ releaseExpansionConcurrencySlot({
820
+ originSessionKey,
821
+ requestId,
822
+ });
687
823
  }
688
824
  },
689
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;
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 = (