condukt 0.16.9 → 0.16.11

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.
Files changed (35) hide show
  1. package/dist/runtimes/copilot/copilot-adapter.d.ts.map +1 -1
  2. package/dist/runtimes/copilot/copilot-adapter.js +12 -0
  3. package/dist/runtimes/copilot/copilot-adapter.js.map +1 -1
  4. package/dist/runtimes/copilot/copilot-backend.d.ts +93 -2
  5. package/dist/runtimes/copilot/copilot-backend.d.ts.map +1 -1
  6. package/dist/runtimes/copilot/index.d.ts +3 -3
  7. package/dist/runtimes/copilot/index.d.ts.map +1 -1
  8. package/dist/runtimes/copilot/index.js +8 -1
  9. package/dist/runtimes/copilot/index.js.map +1 -1
  10. package/dist/runtimes/copilot/sdk-backend.d.ts +8 -2
  11. package/dist/runtimes/copilot/sdk-backend.d.ts.map +1 -1
  12. package/dist/runtimes/copilot/sdk-backend.js +902 -117
  13. package/dist/runtimes/copilot/sdk-backend.js.map +1 -1
  14. package/dist/runtimes/copilot/subagents.d.ts +94 -1
  15. package/dist/runtimes/copilot/subagents.d.ts.map +1 -1
  16. package/dist/runtimes/copilot/subagents.js +138 -10
  17. package/dist/runtimes/copilot/subagents.js.map +1 -1
  18. package/dist/src/agent-node.d.ts +3 -2
  19. package/dist/src/agent-node.d.ts.map +1 -1
  20. package/dist/src/agent-node.js +4 -0
  21. package/dist/src/agent-node.js.map +1 -1
  22. package/dist/src/agent.d.ts.map +1 -1
  23. package/dist/src/agent.js +4 -0
  24. package/dist/src/agent.js.map +1 -1
  25. package/dist/src/index.d.ts +1 -1
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js.map +1 -1
  28. package/dist/src/panel-node.d.ts +4 -2
  29. package/dist/src/panel-node.d.ts.map +1 -1
  30. package/dist/src/panel-node.js +4 -0
  31. package/dist/src/panel-node.js.map +1 -1
  32. package/dist/src/types.d.ts +92 -4
  33. package/dist/src/types.d.ts.map +1 -1
  34. package/dist/src/types.js.map +1 -1
  35. package/package.json +1 -1
@@ -49,9 +49,45 @@ exports.SdkBackend = void 0;
49
49
  const cp = __importStar(require("child_process"));
50
50
  const fs = __importStar(require("fs"));
51
51
  const path = __importStar(require("path"));
52
+ const node_crypto_1 = require("node:crypto");
52
53
  const types_1 = require("../../src/types");
53
54
  const lifecycle_events_1 = require("./lifecycle-events");
54
55
  const subagents_1 = require("./subagents");
56
+ // The CLI requires at least four messages before history.compact() can make
57
+ // progress. This is a runtime constraint, not a tuned safety threshold.
58
+ const MIN_MESSAGES_FOR_COMPACTION = 4;
59
+ function parseContextUsage(data) {
60
+ if (!data)
61
+ return undefined;
62
+ const currentTokens = data.currentTokens;
63
+ const tokenLimit = data.tokenLimit;
64
+ const messagesLength = data.messagesLength;
65
+ if (typeof currentTokens !== 'number'
66
+ || !Number.isFinite(currentTokens)
67
+ || currentTokens < 0
68
+ || typeof tokenLimit !== 'number'
69
+ || !Number.isFinite(tokenLimit)
70
+ || tokenLimit <= 0
71
+ || typeof messagesLength !== 'number'
72
+ || !Number.isFinite(messagesLength)
73
+ || messagesLength < 0) {
74
+ return undefined;
75
+ }
76
+ return {
77
+ currentTokens,
78
+ tokenLimit,
79
+ messagesLength,
80
+ ...(nonNegativeFiniteNumber(data.systemTokens) !== undefined
81
+ ? { systemTokens: data.systemTokens }
82
+ : {}),
83
+ ...(nonNegativeFiniteNumber(data.conversationTokens) !== undefined
84
+ ? { conversationTokens: data.conversationTokens }
85
+ : {}),
86
+ ...(nonNegativeFiniteNumber(data.toolDefinitionsTokens) !== undefined
87
+ ? { toolDefinitionsTokens: data.toolDefinitionsTokens }
88
+ : {}),
89
+ };
90
+ }
55
91
  function normalizeSdkEvent(event) {
56
92
  if (typeof event !== 'object' || event === null)
57
93
  return {};
@@ -64,15 +100,114 @@ function normalizeSdkEvent(event) {
64
100
  : {}),
65
101
  };
66
102
  }
103
+ function isChildModelRequest(context) {
104
+ if (context.interactionType === 'conversation-compaction')
105
+ return false;
106
+ if (context.interactionType === 'conversation-subagent'
107
+ || context.interactionType === 'conversation-sampling') {
108
+ return true;
109
+ }
110
+ // Generic child traffic must carry both sides of its parent relationship.
111
+ // Treat incomplete or ambiguous provenance as parent traffic so uncertainty
112
+ // cannot bypass parent guards.
113
+ return context.agentId !== undefined && context.parentAgentId !== undefined;
114
+ }
115
+ function modelRequestPurpose(interactionType, child, retry) {
116
+ if (interactionType === 'conversation-compaction')
117
+ return 'compaction';
118
+ if (retry)
119
+ return 'retry';
120
+ return child ? 'child' : 'normal-turn';
121
+ }
122
+ function observedRequestBodyTelemetry(request) {
123
+ const contentLength = request.headers.get('content-length');
124
+ const parsedLength = contentLength === null ? undefined : Number(contentLength);
125
+ return Number.isFinite(parsedLength) && parsedLength !== undefined && parsedLength >= 0
126
+ ? { observedRequestBodyBytes: parsedLength }
127
+ : {};
128
+ }
129
+ function createObservedRequestHandler(RequestHandler, WebSocketForwarder, observe, usageFor, refreshParentUsage, retryState, canDispatch) {
130
+ return new class extends RequestHandler {
131
+ parentObservationTail = Promise.resolve();
132
+ async sendRequest(request, context) {
133
+ let serialized = observedRequestBodyTelemetry(request);
134
+ try {
135
+ const body = await request.clone().arrayBuffer();
136
+ serialized = {
137
+ observedRequestBodyBytes: body.byteLength,
138
+ observedRequestBodySha256: (0, node_crypto_1.createHash)('sha256')
139
+ .update(new Uint8Array(body))
140
+ .digest('hex'),
141
+ };
142
+ }
143
+ catch {
144
+ // A bodyless or non-cloneable request still carries content-length when known.
145
+ }
146
+ await this.observeRequest(context, serialized);
147
+ return super.sendRequest(request, context);
148
+ }
149
+ async openWebSocket(context) {
150
+ await this.observeRequest(context, {});
151
+ return new WebSocketForwarder(context);
152
+ }
153
+ observeRequest(context, serialized) {
154
+ const child = isChildModelRequest(context);
155
+ if (child)
156
+ return this.observeRequestNow(context, serialized, true);
157
+ const previous = this.parentObservationTail;
158
+ const observation = previous.then(() => this.observeRequestNow(context, serialized, false));
159
+ this.parentObservationTail = observation.catch(() => undefined);
160
+ return observation;
161
+ }
162
+ async observeRequestNow(context, serialized, child) {
163
+ const compaction = context.interactionType === 'conversation-compaction';
164
+ const retryAgentId = child ? context.agentId : undefined;
165
+ const retry = compaction || (child && retryAgentId === undefined)
166
+ ? false
167
+ : retryState(retryAgentId, false);
168
+ const purpose = modelRequestPurpose(context.interactionType, child, retry);
169
+ const childUsage = child && context.agentId !== undefined
170
+ ? usageFor(context.agentId)
171
+ : undefined;
172
+ const measurement = child
173
+ ? childUsage === undefined
174
+ ? undefined
175
+ : { usage: childUsage, observedInterRequestGrowth: 0 }
176
+ : await refreshParentUsage(purpose);
177
+ const usage = measurement?.usage;
178
+ observe({
179
+ requestId: context.requestId,
180
+ sessionId: context.sessionId,
181
+ agentId: context.agentId,
182
+ parentAgentId: context.parentAgentId,
183
+ purpose,
184
+ interactionType: context.interactionType,
185
+ currentTokens: usage?.currentTokens,
186
+ tokenLimit: usage?.tokenLimit,
187
+ messagesLength: usage?.messagesLength,
188
+ systemTokens: usage?.systemTokens,
189
+ conversationTokens: usage?.conversationTokens,
190
+ toolDefinitionsTokens: usage?.toolDefinitionsTokens,
191
+ ...serialized,
192
+ });
193
+ if (!canDispatch(context, purpose, child, measurement)) {
194
+ throw new Error(`Blocked ${purpose} model request ${context.requestId}: parent context headroom has not been restored`);
195
+ }
196
+ if (retry)
197
+ retryState(retryAgentId, true);
198
+ }
199
+ }();
200
+ }
67
201
  const NAMED_SDK_EVENTS = new Set([
68
202
  'assistant.message', 'assistant.message_delta',
69
203
  'assistant.reasoning', 'assistant.reasoning_delta',
204
+ 'assistant.turn_retry',
70
205
  'assistant.intent', 'assistant.usage',
71
206
  'tool.execution_start', 'tool.execution_complete',
72
207
  'tool.execution_partial_result',
73
208
  'session.idle', 'session.task_complete', 'session.error',
74
209
  'model.call_failure', 'abort',
75
- 'session.compaction_start', 'session.compaction_complete',
210
+ 'session.usage_info', 'session.compaction_start', 'session.compaction_complete',
76
211
  'subagent.started', 'subagent.completed', 'subagent.failed',
77
212
  'permission.requested',
78
213
  ]);
@@ -336,6 +471,10 @@ class SdkBackend {
336
471
  mcpConfigPath;
337
472
  configDirectory;
338
473
  subagentRoster;
474
+ complementaryModelPolicy;
475
+ subagentsEnabled;
476
+ maxDepth;
477
+ maxConcurrency;
339
478
  extraPathDirs;
340
479
  pathTools;
341
480
  logger;
@@ -345,6 +484,11 @@ class SdkBackend {
345
484
  this.mcpConfigPath = options.mcpConfigPath;
346
485
  this.configDirectory = options.configDir;
347
486
  this.subagentRoster = options.subagentRoster;
487
+ this.complementaryModelPolicy = options.complementaryModelPolicy
488
+ ?? subagents_1.DEFAULT_COMPLEMENTARY_MODEL_POLICY;
489
+ this.subagentsEnabled = options.subagentsEnabled ?? true;
490
+ this.maxDepth = options.maxDepth;
491
+ this.maxConcurrency = options.maxConcurrency;
348
492
  this.extraPathDirs = options.extraPathDirs ?? [];
349
493
  this.pathTools = options.pathTools ?? [];
350
494
  this.logger = options.logger ?? types_1.NO_OP_LOGGER;
@@ -366,7 +510,7 @@ class SdkBackend {
366
510
  if (cached)
367
511
  return cached;
368
512
  const resolution = this.resolveModelCapabilitiesUncached(client, model).catch((err) => {
369
- this.logger.warn('Copilot model capability resolution failed; using proportional compaction thresholds only', {
513
+ this.logger.warn('Copilot model capability resolution failed; awaiting runtime context accounting', {
370
514
  model,
371
515
  reason: 'capability_resolution_failed',
372
516
  error: err instanceof Error ? err.message : String(err),
@@ -382,7 +526,7 @@ class SdkBackend {
382
526
  return {};
383
527
  const selected = models.find(candidate => candidate.id === model);
384
528
  if (!selected) {
385
- this.logger.warn('Copilot model capability lookup failed; using proportional compaction thresholds only', {
529
+ this.logger.warn('Copilot model capability lookup failed; awaiting runtime context accounting', {
386
530
  model,
387
531
  reason: 'model_not_found',
388
532
  });
@@ -402,7 +546,7 @@ class SdkBackend {
402
546
  ? contextWindowLimit
403
547
  : undefined;
404
548
  if (source === undefined || discoveredLimit === undefined) {
405
- this.logger.warn('Copilot model capability lookup returned no usable token limit; using proportional compaction thresholds only', {
549
+ this.logger.warn('Copilot model capability lookup returned no usable token limit; awaiting runtime context accounting', {
406
550
  model,
407
551
  reason: 'invalid_limit',
408
552
  maxPromptTokens: promptLimit,
@@ -410,28 +554,14 @@ class SdkBackend {
410
554
  });
411
555
  return {};
412
556
  }
413
- const promptTokenCeiling = Math.floor(discoveredLimit * 0.80);
414
- if (!isPositiveFiniteNumber(promptTokenCeiling)) {
415
- this.logger.warn('Copilot model capability lookup produced no usable prompt-token ceiling; using proportional compaction thresholds only', {
416
- model,
417
- reason: 'invalid_computed_ceiling',
418
- discoveredLimit,
419
- limitSource: source,
420
- });
421
- return {};
422
- }
423
- this.logger.info('Resolved Copilot model prompt-token ceiling', {
557
+ this.logger.info('Resolved Copilot model prompt-token limit', {
424
558
  model,
425
- discoveredLimit,
559
+ reportedPromptTokenLimit: discoveredLimit,
426
560
  limitSource: source,
427
- promptTokenCeiling,
428
561
  });
429
562
  return {
430
- modelCapabilities: {
431
- limits: {
432
- max_prompt_tokens: promptTokenCeiling,
433
- },
434
- },
563
+ reportedPromptTokenLimit: Math.floor(discoveredLimit),
564
+ limitSource: source,
435
565
  };
436
566
  }
437
567
  listModels(client) {
@@ -443,7 +573,7 @@ class SdkBackend {
443
573
  return await client.listModels();
444
574
  }
445
575
  catch (err) {
446
- this.logger.warn('Copilot model capability discovery failed; using proportional compaction thresholds only', {
576
+ this.logger.warn('Copilot model capability discovery failed; awaiting runtime context accounting', {
447
577
  reason: 'list_models_failed',
448
578
  error: err instanceof Error ? err.message : String(err),
449
579
  });
@@ -456,7 +586,7 @@ class SdkBackend {
456
586
  if (options?.signal?.aborted) {
457
587
  throw new Error('Session creation aborted');
458
588
  }
459
- return new SdkSession(config, this.mcpConfigPath, this.configDirectory, this.subagentRoster, this.extraPathDirs, this.pathTools, this.logger, (client, model) => this.resolveModelCapabilities(client, model));
589
+ return new SdkSession(config, this.mcpConfigPath, this.configDirectory, this.subagentRoster, this.complementaryModelPolicy, this.subagentsEnabled, this.maxDepth, this.maxConcurrency, this.extraPathDirs, this.pathTools, this.logger, (client, model) => this.resolveModelCapabilities(client, model));
460
590
  }
461
591
  }
462
592
  exports.SdkBackend = SdkBackend;
@@ -481,6 +611,10 @@ class SdkSession {
481
611
  mcpConfigPath;
482
612
  configDirectory;
483
613
  backendRoster;
614
+ complementaryModelPolicy;
615
+ backendSubagentsEnabled;
616
+ backendMaxDepth;
617
+ backendMaxConcurrency;
484
618
  extraPathDirs;
485
619
  pathTools;
486
620
  logger;
@@ -490,11 +624,23 @@ class SdkSession {
490
624
  compactionTimer = null;
491
625
  abortGraceTimer = null;
492
626
  compactionInProgress = false;
627
+ parentCompactionGeneration = 0;
628
+ verifiedParentCompactionGeneration = 0;
629
+ compactionBaselinePromise;
630
+ proactiveCompactionPromise;
631
+ reportedPromptTokenLimit;
632
+ reportedPromptTokenLimitSource;
633
+ adaptiveCompactionPolicy;
634
+ previousParentRequestTokens;
635
+ largestObservedInterRequestGrowth = 0;
636
+ parentUsage;
637
+ latestContextDiagnostics;
638
+ childUsage = new Map();
639
+ retryingAgentIds = new Set();
640
+ retryingParent = false;
493
641
  turnSettled = false;
494
- compactionRecoveryInProgress = false;
495
642
  aborted = false;
496
643
  _turnText = new Map();
497
- intentionalAborts = new WeakSet();
498
644
  /**
499
645
  * Maps toolCallId -> toolName for attributing tool_complete events.
500
646
  * Populated from assistant.message toolRequests and tool.execution_start.
@@ -515,11 +661,57 @@ class SdkSession {
515
661
  // SDK manages the CLI process internally; no direct PID access
516
662
  return null;
517
663
  }
518
- constructor(config, mcpConfigPath, configDirectory, backendRoster, extraPathDirs, pathTools, logger, resolveModelCapabilities) {
664
+ history = {
665
+ summarizeForHandoff: async () => {
666
+ const sdkSession = this.activeSdkSession();
667
+ const result = await sdkSession.rpc.history.summarizeForHandoff();
668
+ this.logger.info('Copilot history handoff summary completed', {
669
+ summaryLength: result.summary.length,
670
+ });
671
+ return result.summary;
672
+ },
673
+ truncate: async (eventId) => {
674
+ const sdkSession = this.activeSdkSession();
675
+ const result = await sdkSession.rpc.history.truncate({ eventId });
676
+ this.logger.warn('Copilot history truncation completed', {
677
+ eventId,
678
+ eventsRemoved: result.eventsRemoved,
679
+ });
680
+ return result.eventsRemoved;
681
+ },
682
+ };
683
+ metadata = {
684
+ getContextAttribution: async () => {
685
+ const result = await this.activeSdkSession().rpc.metadata.getContextAttribution();
686
+ return result.contextAttribution ?? null;
687
+ },
688
+ getContextHeaviestMessages: async (limit) => {
689
+ return this.activeSdkSession().rpc.metadata.getContextHeaviestMessages(limit === undefined ? {} : { limit });
690
+ },
691
+ recomputeContextTokens: async (modelId = this.config.model) => {
692
+ return this.activeSdkSession().rpc.metadata.recomputeContextTokens({ modelId });
693
+ },
694
+ };
695
+ usage = {
696
+ getMetrics: async () => {
697
+ return this.activeSdkSession().rpc.usage.getMetrics();
698
+ },
699
+ };
700
+ activeSdkSession() {
701
+ const sdkSession = this._sdkSession;
702
+ if (!sdkSession)
703
+ throw new Error('SDK session is not active');
704
+ return sdkSession;
705
+ }
706
+ constructor(config, mcpConfigPath, configDirectory, backendRoster, complementaryModelPolicy, backendSubagentsEnabled, backendMaxDepth, backendMaxConcurrency, extraPathDirs, pathTools, logger, resolveModelCapabilities) {
519
707
  this.config = config;
520
708
  this.mcpConfigPath = mcpConfigPath;
521
709
  this.configDirectory = configDirectory;
522
710
  this.backendRoster = backendRoster;
711
+ this.complementaryModelPolicy = complementaryModelPolicy;
712
+ this.backendSubagentsEnabled = backendSubagentsEnabled;
713
+ this.backendMaxDepth = backendMaxDepth;
714
+ this.backendMaxConcurrency = backendMaxConcurrency;
523
715
  this.extraPathDirs = extraPathDirs;
524
716
  this.pathTools = pathTools;
525
717
  this.logger = logger;
@@ -542,6 +734,16 @@ class SdkSession {
542
734
  this._toolCallNames.clear();
543
735
  this._callIdToParent.clear();
544
736
  this._pendingPartials.clear();
737
+ this.parentUsage = undefined;
738
+ this.latestContextDiagnostics = undefined;
739
+ this.adaptiveCompactionPolicy = undefined;
740
+ this.previousParentRequestTokens = undefined;
741
+ this.largestObservedInterRequestGrowth = 0;
742
+ this.childUsage.clear();
743
+ this.retryingAgentIds.clear();
744
+ this.retryingParent = false;
745
+ this.compactionBaselinePromise = undefined;
746
+ this.proactiveCompactionPromise = undefined;
545
747
  this.turnSettled = false;
546
748
  this._run(prompt).catch((err) => {
547
749
  this.fail(err instanceof Error ? err : new Error(String(err)), 'session.run');
@@ -558,7 +760,7 @@ class SdkSession {
558
760
  const sdkModuleName = '@github/copilot-sdk';
559
761
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
560
762
  const dynamicImport = new Function('specifier', 'return import(specifier)');
561
- const { CopilotClient, RuntimeConnection, approveAll } = await dynamicImport(sdkModuleName);
763
+ const { CopilotClient, CopilotRequestHandler, CopilotWebSocketForwarder, RuntimeConnection, approveAll, } = await dynamicImport(sdkModuleName);
562
764
  // ---------------------------------------------------------------
563
765
  // Build hardened environment (strip NODE_OPTIONS, extend PATH)
564
766
  // ---------------------------------------------------------------
@@ -593,6 +795,55 @@ class SdkSession {
593
795
  connection: RuntimeConnection.forStdio(),
594
796
  env,
595
797
  logLevel: 'warning',
798
+ requestHandler: createObservedRequestHandler(CopilotRequestHandler, CopilotWebSocketForwarder, (telemetry) => this.logModelRequest(telemetry), (agentId) => agentId ? this.childUsage.get(agentId) : this.parentUsage, (purpose) => {
799
+ const sdkSession = this._sdkSession;
800
+ return sdkSession && (purpose === 'compaction'
801
+ || this.config.compactionMode === 'adaptive'
802
+ || this.shouldCaptureRequestDiagnostics(purpose))
803
+ ? this.refreshParentUsageBeforeRequest(sdkSession, purpose)
804
+ : Promise.resolve(this.parentUsage === undefined
805
+ ? undefined
806
+ : { usage: this.parentUsage, observedInterRequestGrowth: 0 });
807
+ }, (agentId, consume) => {
808
+ if (agentId) {
809
+ const retry = this.retryingAgentIds.has(agentId);
810
+ if (retry && consume)
811
+ this.retryingAgentIds.delete(agentId);
812
+ return retry;
813
+ }
814
+ const retry = this.retryingParent;
815
+ if (retry && consume)
816
+ this.retryingParent = false;
817
+ return retry;
818
+ },
819
+ // Capability discovery also uses this handler before createSession has
820
+ // returned a session handle. Scope the allowance to traffic that has no
821
+ // session provenance so a delayed createSession cannot open a parent turn.
822
+ (context, purpose, child, measurement) => {
823
+ if (this.aborted || this.turnSettled)
824
+ return false;
825
+ if (this._sdkSession === null) {
826
+ return context.sessionId === undefined && context.agentId === undefined;
827
+ }
828
+ if (child)
829
+ return true;
830
+ const usage = measurement?.usage;
831
+ if (purpose === 'compaction') {
832
+ return usage !== undefined && usage.currentTokens < usage.tokenLimit;
833
+ }
834
+ if (this.parentCompactionGeneration > this.verifiedParentCompactionGeneration
835
+ || this.compactionInProgress)
836
+ return false;
837
+ if (measurement === undefined)
838
+ return false;
839
+ const measuredUsage = measurement.usage;
840
+ if (measuredUsage.currentTokens + measurement.observedInterRequestGrowth
841
+ >= measuredUsage.tokenLimit)
842
+ return false;
843
+ if (this.config.compactionMode !== 'adaptive')
844
+ return true;
845
+ return this.canDispatchParentRequest(measuredUsage);
846
+ }),
596
847
  });
597
848
  this._client = client;
598
849
  // ---------------------------------------------------------------
@@ -617,14 +868,66 @@ class SdkSession {
617
868
  else {
618
869
  roster = this.backendRoster;
619
870
  }
620
- const { modelCapabilities } = await this.resolveModelCapabilities(client, this.config.model);
871
+ const { reportedPromptTokenLimit, limitSource, } = await this.resolveModelCapabilities(client, this.config.model);
872
+ this.reportedPromptTokenLimit = reportedPromptTokenLimit;
873
+ this.reportedPromptTokenLimitSource = limitSource;
874
+ const subagentsEnabled = this.config.subagentsEnabled ?? this.backendSubagentsEnabled;
875
+ const tieredCustomAgents = subagentsEnabled
876
+ ? (0, subagents_1.resolveTieredCustomAgents)(this.config.model, this.complementaryModelPolicy)
877
+ : undefined;
878
+ if (tieredCustomAgents !== undefined) {
879
+ const resolution = tieredCustomAgents.reviewResolution;
880
+ const fields = {
881
+ subagent: 'review',
882
+ sessionModel: resolution.sourceModel,
883
+ resolvedModel: resolution.resolvedModel,
884
+ usedCliFallback: resolution.usedCliFallback,
885
+ ...(resolution.source !== undefined
886
+ ? { sourceLab: resolution.source.lab, sourceTier: resolution.source.tier }
887
+ : {}),
888
+ ...(resolution.complement !== undefined
889
+ ? { resolvedLab: resolution.complement.lab, resolvedTier: resolution.complement.tier }
890
+ : {}),
891
+ };
892
+ if (resolution.usedCliFallback) {
893
+ this.logger.warn('Falling back to CLI complementary custom-agent model resolution', fields);
894
+ }
895
+ else {
896
+ this.logger.info('Resolved complementary custom-agent model', fields);
897
+ }
898
+ }
899
+ const customAgents = new Map();
900
+ for (const agent of tieredCustomAgents?.agents ?? [])
901
+ customAgents.set(agent.name, agent);
902
+ for (const agent of this.config.customAgents ?? []) {
903
+ if (!subagentsEnabled || customAgents.has(agent.name))
904
+ customAgents.set(agent.name, agent);
905
+ }
906
+ const excludedBuiltinAgents = subagentsEnabled
907
+ ? [...new Set(['explore', 'research', ...(this.config.excludedBuiltinAgents ?? [])])]
908
+ : this.config.excludedBuiltinAgents !== undefined
909
+ ? [...this.config.excludedBuiltinAgents]
910
+ : undefined;
911
+ const permissionHandler = subagentsEnabled
912
+ ? approveAll
913
+ : (request, invocation) => {
914
+ const toolName = request.kind === 'custom-tool'
915
+ || request.kind === 'mcp'
916
+ || request.kind === 'hook'
917
+ ? request.toolName
918
+ : undefined;
919
+ if (toolName === 'task') {
920
+ return {
921
+ kind: 'reject',
922
+ feedback: 'Model-issued sub-agent dispatch is disabled for this session.',
923
+ };
924
+ }
925
+ return approveAll(request, invocation);
926
+ };
621
927
  const sessionConfig = {
622
928
  model: this.config.model,
623
929
  streaming: true,
624
- // This applies only to the parent session. The installed SDK's subagent
625
- // settings surface cannot carry a capability override to child sessions.
626
- ...(modelCapabilities !== undefined ? { modelCapabilities } : {}),
627
- onPermissionRequest: approveAll,
930
+ onPermissionRequest: permissionHandler,
628
931
  workingDirectory: this.config.cwd,
629
932
  reasoningEffort: this.config.thinkingBudget,
630
933
  ...(this.config.contextTier !== undefined
@@ -639,11 +942,14 @@ class SdkSession {
639
942
  ...(this.config.availableTools !== undefined
640
943
  ? { availableTools: [...this.config.availableTools] }
641
944
  : {}),
642
- ...(this.config.excludedTools !== undefined
643
- ? { excludedTools: [...this.config.excludedTools] }
644
- : {}),
645
- ...(this.config.customAgents !== undefined
646
- ? { customAgents: this.config.customAgents.map(toSdkCustomAgent) }
945
+ ...(() => {
946
+ const excludedTools = new Set(this.config.excludedTools ?? []);
947
+ if (!subagentsEnabled)
948
+ excludedTools.add('task');
949
+ return excludedTools.size > 0 ? { excludedTools: [...excludedTools] } : {};
950
+ })(),
951
+ ...(customAgents.size > 0
952
+ ? { customAgents: [...customAgents.values()].map(toSdkCustomAgent) }
647
953
  : {}),
648
954
  ...(this.config.defaultAgent !== undefined
649
955
  ? {
@@ -654,22 +960,27 @@ class SdkSession {
654
960
  },
655
961
  }
656
962
  : {}),
657
- ...(this.config.excludedBuiltinAgents !== undefined
658
- ? { excludedBuiltinAgents: [...this.config.excludedBuiltinAgents] }
963
+ ...(excludedBuiltinAgents !== undefined
964
+ ? { excludedBuiltinAgents }
659
965
  : {}),
660
966
  ...(mcpServers !== undefined ? { mcpServers } : {}),
661
967
  // The SDK defaults this on, so every commit an agent composes carries a
662
968
  // Co-authored-by trailer. Output should read as the repository owner's work.
663
969
  coauthorEnabled: false,
664
- // Layer proportional compaction over the discovered absolute pre-send ceiling.
665
- // For gpt-5.6-sol, the SDK's 1,050,000-token denominator starts background
666
- // compaction at 630,000 (60%), while 80% of the provider's 922,000 prompt
667
- // limit caps pre-send prompts at 737,600 before the 787,500 blocking fraction.
668
- infiniteSessions: {
669
- enabled: true,
670
- backgroundCompactionThreshold: 0.60,
671
- bufferExhaustionThreshold: 0.75,
672
- },
970
+ // Stock documented settings are the default and do not override model
971
+ // capabilities. The reduced legacy thresholds remain available for
972
+ // comparative diagnostics; the measured adaptive controller is opt-in.
973
+ infiniteSessions: this.config.compactionMode === 'aggressive'
974
+ ? {
975
+ enabled: true,
976
+ backgroundCompactionThreshold: 0.60,
977
+ bufferExhaustionThreshold: 0.75,
978
+ }
979
+ : {
980
+ enabled: true,
981
+ backgroundCompactionThreshold: 0.80,
982
+ bufferExhaustionThreshold: 0.95,
983
+ },
673
984
  // Registered before createSession issues its RPC, closing the early-event
674
985
  // gap for session.start and *_loaded events.
675
986
  onEvent: (event) => this.handleEarlyEvent(normalizeSdkEvent(event)),
@@ -683,16 +994,50 @@ class SdkSession {
683
994
  return;
684
995
  }
685
996
  this._sdkSession = sdkSession;
997
+ const resolvedRoster = roster === undefined || roster === false
998
+ ? roster
999
+ : (0, subagents_1.resolveSubagentRosterModels)(roster, this.config.model, this.complementaryModelPolicy);
1000
+ if (resolvedRoster !== undefined && resolvedRoster !== false) {
1001
+ for (const resolution of resolvedRoster.resolutions) {
1002
+ const fields = {
1003
+ subagent: resolution.subagent,
1004
+ sessionModel: resolution.sourceModel,
1005
+ resolvedModel: resolution.resolvedModel,
1006
+ usedCliFallback: resolution.usedCliFallback,
1007
+ ...(resolution.source !== undefined
1008
+ ? { sourceLab: resolution.source.lab, sourceTier: resolution.source.tier }
1009
+ : {}),
1010
+ ...(resolution.complement !== undefined
1011
+ ? { resolvedLab: resolution.complement.lab, resolvedTier: resolution.complement.tier }
1012
+ : {}),
1013
+ };
1014
+ if (resolution.usedCliFallback) {
1015
+ this.logger.warn('Falling back to CLI complementary sub-agent model resolution', fields);
1016
+ }
1017
+ else {
1018
+ this.logger.info('Resolved complementary sub-agent model', fields);
1019
+ }
1020
+ }
1021
+ }
1022
+ const maxDepth = this.config.maxDepth ?? this.backendMaxDepth;
1023
+ const maxConcurrency = this.config.maxConcurrency ?? this.backendMaxConcurrency;
1024
+ const subagentSettings = {
1025
+ ...(resolvedRoster !== undefined && resolvedRoster !== false
1026
+ ? { agents: resolvedRoster.roster }
1027
+ : {}),
1028
+ ...(maxDepth !== undefined ? { maxDepth } : {}),
1029
+ ...(maxConcurrency !== undefined ? { maxConcurrency } : {}),
1030
+ };
686
1031
  // Apply the live override before any prompt can dispatch a subagent.
687
1032
  // This experimental RPC degrades safely if the installed CLI rejects it.
688
- if (roster !== undefined && roster !== false) {
1033
+ if (Object.keys(subagentSettings).length > 0) {
689
1034
  try {
690
1035
  await sdkSession.rpc.tools.updateSubagentSettings({
691
- subagents: { agents: roster },
1036
+ subagents: subagentSettings,
692
1037
  });
693
1038
  }
694
1039
  catch (err) {
695
- this.logger.warn('Failed to apply Copilot subagent roster; using default settings', {
1040
+ this.logger.warn('Failed to apply Copilot subagent settings; using default settings', {
696
1041
  error: err instanceof Error ? err.message : String(err),
697
1042
  });
698
1043
  }
@@ -722,6 +1067,9 @@ class SdkSession {
722
1067
  // ---------------------------------------------------------------
723
1068
  // Send the prompt (fire-and-forget; events stream via handlers)
724
1069
  // ---------------------------------------------------------------
1070
+ if (this.config.compactionMode === 'adaptive'
1071
+ && !(await this.ensureCompactionHeadroom(sdkSession, 'pre-send')))
1072
+ return;
725
1073
  await sdkSession.send({ prompt });
726
1074
  }
727
1075
  /**
@@ -874,7 +1222,7 @@ class SdkSession {
874
1222
  });
875
1223
  // --- Session idle (agent finished all work) ---
876
1224
  sdkSession.on('session.idle', (e) => {
877
- if (!isActive() || this.compactionRecoveryInProgress)
1225
+ if (!isActive())
878
1226
  return;
879
1227
  this.resetHeartbeat();
880
1228
  if (!e.agentId)
@@ -882,7 +1230,7 @@ class SdkSession {
882
1230
  });
883
1231
  // --- task_complete → idle (some models fire this instead of session.idle) ---
884
1232
  sdkSession.on('session.task_complete', (e) => {
885
- if (!isActive() || this.compactionRecoveryInProgress)
1233
+ if (!isActive())
886
1234
  return;
887
1235
  this.resetHeartbeat();
888
1236
  if (e.agentId)
@@ -904,13 +1252,11 @@ class SdkSession {
904
1252
  // The CLI normally follows an abort event with session.idle. If that
905
1253
  // terminal event is lost, fail quickly rather than waiting for heartbeat.
906
1254
  sdkSession.on('abort', (e) => {
907
- if (!isActive() || this.compactionRecoveryInProgress)
1255
+ if (!isActive())
908
1256
  return;
909
1257
  this.resetHeartbeat();
910
1258
  if (this.logAgentScopedFailure(e))
911
1259
  return;
912
- if (this.intentionalAborts.delete(sdkSession))
913
- return;
914
1260
  if (this.abortGraceTimer)
915
1261
  clearTimeout(this.abortGraceTimer);
916
1262
  this.abortGraceTimer = setTimeout(() => {
@@ -963,7 +1309,34 @@ class SdkSession {
963
1309
  Object.assign(error, { statusCode });
964
1310
  if (errorCode !== undefined)
965
1311
  Object.assign(error, { errorCode });
966
- this.fail(error, 'model.call_failure', data);
1312
+ const compactionFailed = data?.initiator === 'compaction'
1313
+ || data?.source === 'compaction'
1314
+ || this.compactionInProgress
1315
+ || this.proactiveCompactionPromise !== undefined;
1316
+ this.fail(error, compactionFailed ? 'compaction.model.call_failure' : 'model.call_failure', {
1317
+ ...data,
1318
+ ...(compactionFailed ? { requestPurpose: 'compaction' } : {}),
1319
+ ...(this.parentUsage !== undefined
1320
+ ? {
1321
+ currentTokens: this.parentUsage.currentTokens,
1322
+ tokenLimit: this.parentUsage.tokenLimit,
1323
+ messagesLength: this.parentUsage.messagesLength,
1324
+ }
1325
+ : {}),
1326
+ });
1327
+ });
1328
+ sdkSession.on('session.usage_info', (e) => {
1329
+ if (!isActive())
1330
+ return;
1331
+ this.handleContextUsage(sdkSession, e);
1332
+ });
1333
+ sdkSession.on('assistant.turn_retry', (e) => {
1334
+ if (!isActive())
1335
+ return;
1336
+ if (e.agentId)
1337
+ this.retryingAgentIds.add(e.agentId);
1338
+ else
1339
+ this.retryingParent = true;
967
1340
  });
968
1341
  // --- Context compaction (infinite sessions) ---
969
1342
  // During compaction the model goes silent. SUSPEND the heartbeat entirely
@@ -971,10 +1344,12 @@ class SdkSession {
971
1344
  sdkSession.on('session.compaction_start', (e) => {
972
1345
  if (!isActive())
973
1346
  return;
974
- this.resetHeartbeat();
975
1347
  if (e.agentId)
976
1348
  return;
1349
+ this.resetHeartbeat();
977
1350
  this.compactionInProgress = true;
1351
+ this.parentCompactionGeneration += 1;
1352
+ this.compactionBaselinePromise = this.captureContextDiagnostics(sdkSession, 'pre-compaction');
978
1353
  // SUSPEND heartbeat — compaction silence is expected
979
1354
  if (this.heartbeatTimer) {
980
1355
  clearTimeout(this.heartbeatTimer);
@@ -1005,82 +1380,71 @@ class SdkSession {
1005
1380
  return;
1006
1381
  // If force-compact works, compaction_complete will fire naturally
1007
1382
  }
1008
- catch {
1009
- // Re-check guards after await
1383
+ catch (err) {
1010
1384
  if (!isActive())
1011
1385
  return;
1012
- try {
1013
- process.stderr.write('[SdkBackend] Force compact failed — aborting + re-sending\n');
1014
- }
1015
- catch { /* */ }
1016
- // Escalate: abort current message, then re-send to nudge model.
1017
- // Uses RAW SDK abort (session stays alive), NOT this.abort() (which tears down).
1018
- // SDK docs: "The session remains valid and can continue to be used for new messages."
1019
- // Wait for idle after abort before sending, to avoid racing with in-flight processing.
1020
- this.compactionInProgress = false;
1021
- this.compactionRecoveryInProgress = true;
1022
- this._turnText.clear();
1023
- try {
1024
- this.intentionalAborts.add(session);
1025
- await session.abort();
1026
- // Some SDK versions emit abort synchronously, others just after the
1027
- // request resolves. Keep the marker through the settling window.
1028
- if (this.abortGraceTimer) {
1029
- clearTimeout(this.abortGraceTimer);
1030
- this.abortGraceTimer = null;
1031
- }
1032
- // WeakSet entries are removed when the delayed intentional abort event
1033
- // arrives. The handle itself becomes collectible after session cleanup.
1034
- // Wait briefly for the abort to settle before re-sending.
1035
- // The SDK resolves abort() on acknowledgement, not on idle.
1036
- await new Promise(resolve => setTimeout(resolve, 2000));
1037
- if (!isActive())
1038
- return;
1039
- await session.send({ prompt: 'Continue from where you left off.' });
1040
- this.compactionRecoveryInProgress = false;
1041
- this.resetHeartbeat();
1042
- }
1043
- catch {
1044
- this.compactionRecoveryInProgress = false;
1045
- try {
1046
- process.stderr.write('[SdkBackend] Recovery failed — restarting heartbeat as safety net\n');
1047
- }
1048
- catch { /* */ }
1049
- this.resetHeartbeat(); // Detect if model is truly dead instead of waiting for hours-long hard timeout
1050
- }
1386
+ const usage = this.parentUsage;
1387
+ const detail = err instanceof Error ? err.message : String(err);
1388
+ const fields = {
1389
+ requestPurpose: 'compaction',
1390
+ error: detail,
1391
+ ...(usage !== undefined
1392
+ ? {
1393
+ currentTokens: usage.currentTokens,
1394
+ tokenLimit: usage.tokenLimit,
1395
+ messagesLength: usage.messagesLength,
1396
+ systemTokens: usage.systemTokens,
1397
+ conversationTokens: usage.conversationTokens,
1398
+ toolDefinitionsTokens: usage.toolDefinitionsTokens,
1399
+ }
1400
+ : {}),
1401
+ };
1402
+ this.fail(new Error(`Compaction recovery request failed: ${detail}`), 'compaction.recovery_failure', fields);
1051
1403
  }
1052
1404
  }, 3 * 60 * 1000);
1053
1405
  });
1054
1406
  sdkSession.on('session.compaction_complete', (e) => {
1055
1407
  if (!isActive())
1056
1408
  return;
1057
- if (e.agentId) {
1058
- this.resetHeartbeat();
1409
+ if (e.agentId)
1059
1410
  return;
1060
- }
1061
1411
  this.compactionInProgress = false;
1062
1412
  if (this.compactionTimer) {
1063
1413
  clearTimeout(this.compactionTimer);
1064
1414
  this.compactionTimer = null;
1065
1415
  }
1066
- // Restart heartbeat — model should resume producing output
1416
+ // Restart heartbeat while the asynchronous verification RPC runs. Any
1417
+ // ordinary parent request remains guarded at the request-handler seam.
1067
1418
  this.resetHeartbeat();
1068
1419
  const data = e.data;
1069
- // Check if compaction actually succeeded
1070
1420
  if (data?.success === false) {
1071
1421
  const errMsg = typeof data.error === 'string' ? data.error : 'unknown reason';
1072
- try {
1073
- process.stderr.write(`[SdkBackend] Compaction failed: ${errMsg}\n`);
1074
- }
1075
- catch { /* */ }
1422
+ const rawStatusCode = data.statusCode;
1423
+ const statusCode = typeof rawStatusCode === 'number'
1424
+ ? ` (HTTP ${rawStatusCode})`
1425
+ : '';
1426
+ const usage = this.parentUsage;
1427
+ const fields = {
1428
+ ...data,
1429
+ requestPurpose: 'compaction',
1430
+ ...(usage !== undefined
1431
+ ? {
1432
+ currentTokens: usage.currentTokens,
1433
+ tokenLimit: usage.tokenLimit,
1434
+ messagesLength: usage.messagesLength,
1435
+ systemTokens: usage.systemTokens,
1436
+ conversationTokens: usage.conversationTokens,
1437
+ toolDefinitionsTokens: usage.toolDefinitionsTokens,
1438
+ }
1439
+ : {}),
1440
+ };
1441
+ this.fail(new Error(`Compaction model call failed${statusCode}: ${errMsg}`), 'compaction.call_failure', fields);
1442
+ return;
1076
1443
  }
1077
- const pre = data?.preCompactionTokens;
1078
- const post = data?.postCompactionTokens;
1079
- const removed = data?.tokensRemoved;
1080
- const summary = pre && post
1081
- ? `${pre} → ${post} tokens${removed != null ? ` (saved ${removed})` : ''}`
1082
- : '';
1083
- this.emit('compaction', 'complete', summary);
1444
+ const generation = this.parentCompactionGeneration;
1445
+ const baselinePromise = this.compactionBaselinePromise;
1446
+ this.compactionBaselinePromise = undefined;
1447
+ void this.verifyCompletedCompaction(sdkSession, data, generation, baselinePromise);
1084
1448
  });
1085
1449
  // --- Subagent lifecycle ---
1086
1450
  // Sub-agents use their own event path (subagent_start/subagent_end).
@@ -1101,7 +1465,18 @@ class SdkSession {
1101
1465
  this.resetHeartbeat();
1102
1466
  const data = e.data;
1103
1467
  const name = String(data?.agentDisplayName ?? data?.agentName ?? 'agent');
1468
+ const agentName = String(data?.agentName ?? name);
1469
+ const model = typeof data?.model === 'string' ? data.model : undefined;
1470
+ const totalTokens = nonNegativeFiniteNumber(data?.totalTokens);
1104
1471
  const toolCallId = typeof data?.toolCallId === 'string' ? data.toolCallId : '';
1472
+ const fields = {
1473
+ agentName,
1474
+ model,
1475
+ cumulativeTokensConsumed: totalTokens,
1476
+ totalToolCalls: nonNegativeFiniteNumber(data?.totalToolCalls),
1477
+ durationMs: nonNegativeFiniteNumber(data?.durationMs),
1478
+ };
1479
+ this.logger.info('Copilot sub-agent cumulative token consumption', fields);
1105
1480
  this.emit('subagent_end', name, { ...data, toolCallId });
1106
1481
  });
1107
1482
  sdkSession.on('subagent.failed', (e) => {
@@ -1265,6 +1640,10 @@ class SdkSession {
1265
1640
  // Only the pre-handle creation window is handled here.
1266
1641
  if (this._sdkSession)
1267
1642
  return;
1643
+ if (e.type === 'session.usage_info') {
1644
+ this.handleContextUsage(undefined, e);
1645
+ return;
1646
+ }
1268
1647
  const eventClass = (0, lifecycle_events_1.classifySdkEvent)(e.type);
1269
1648
  if (eventClass === 'informational')
1270
1649
  return;
@@ -1350,6 +1729,409 @@ class SdkSession {
1350
1729
  || typeof data.error === 'string'
1351
1730
  || (data.error != null && typeof data.error === 'object');
1352
1731
  }
1732
+ canDispatchParentRequest(usage) {
1733
+ const policy = this.adaptiveCompactionPolicy;
1734
+ if (usage === undefined || policy === undefined) {
1735
+ if (usage !== undefined) {
1736
+ this.failUnsafeContext('pre-send', usage, policy, 0, 'fresh parent context accounting was unavailable at the request-handler seam');
1737
+ }
1738
+ else {
1739
+ this.fail(new Error('Blocked parent model request: fresh context accounting was unavailable'), 'pre-send-context-accounting');
1740
+ }
1741
+ return false;
1742
+ }
1743
+ if (usage.currentTokens < policy.threshold)
1744
+ return true;
1745
+ this.failUnsafeContext('pre-send', usage, policy, 0, 'the runtime attempted a parent model request before the required adaptive headroom was restored');
1746
+ return false;
1747
+ }
1748
+ async captureContextDiagnostics(sdkSession, reason) {
1749
+ try {
1750
+ const [attributionResult, heaviestMessages, recomputed, usageMetrics] = await Promise.all([
1751
+ sdkSession.rpc.metadata.getContextAttribution(),
1752
+ sdkSession.rpc.metadata.getContextHeaviestMessages({}),
1753
+ sdkSession.rpc.metadata.recomputeContextTokens({ modelId: this.config.model }),
1754
+ sdkSession.rpc.usage.getMetrics(),
1755
+ ]);
1756
+ const attribution = attributionResult.contextAttribution ?? null;
1757
+ const fixedPromptOverhead = attribution
1758
+ ? Math.max(0, attribution.totalTokens - recomputed.messagesTokenCount)
1759
+ : recomputed.systemTokenCount;
1760
+ const diagnostics = {
1761
+ attribution,
1762
+ heaviestMessages,
1763
+ recomputed,
1764
+ usageMetrics,
1765
+ fixedPromptOverhead,
1766
+ };
1767
+ this.logger.info('Captured Copilot parent context diagnostics', {
1768
+ reason,
1769
+ successfulCompactions: attribution?.compactions.count,
1770
+ fixedPromptOverhead,
1771
+ contextAttribution: attribution,
1772
+ heaviestMessages,
1773
+ recomputedContextTokens: recomputed,
1774
+ usageMetrics,
1775
+ });
1776
+ return diagnostics;
1777
+ }
1778
+ catch (err) {
1779
+ this.logger.warn('Failed to capture Copilot parent context diagnostics', {
1780
+ reason,
1781
+ error: err instanceof Error ? err.message : String(err),
1782
+ });
1783
+ return undefined;
1784
+ }
1785
+ }
1786
+ async measureParentUsage(sdkSession, reason) {
1787
+ const previous = this.parentUsage;
1788
+ try {
1789
+ const [contextResult, diagnostics] = await Promise.all([
1790
+ sdkSession.rpc.metadata.contextInfo({
1791
+ promptTokenLimit: previous?.tokenLimit ?? this.reportedPromptTokenLimit ?? 0,
1792
+ outputTokenLimit: 0,
1793
+ selectedModel: this.config.model,
1794
+ }),
1795
+ this.captureContextDiagnostics(sdkSession, reason),
1796
+ ]);
1797
+ const contextInfo = contextResult.contextInfo;
1798
+ if (!contextInfo || !diagnostics) {
1799
+ this.logger.warn('Copilot parent context measurement returned incomplete context information', {
1800
+ reason,
1801
+ previousCurrentTokens: previous?.currentTokens,
1802
+ hasContextInfo: contextInfo !== null && contextInfo !== undefined,
1803
+ hasDiagnostics: diagnostics !== undefined,
1804
+ });
1805
+ return undefined;
1806
+ }
1807
+ const completeDiagnostics = diagnostics;
1808
+ this.latestContextDiagnostics = completeDiagnostics;
1809
+ const usage = {
1810
+ currentTokens: diagnostics.attribution?.totalTokens ?? diagnostics.recomputed.totalTokens,
1811
+ tokenLimit: contextInfo.promptTokenLimit,
1812
+ messagesLength: previous?.messagesLength ?? 0,
1813
+ systemTokens: diagnostics.recomputed.systemTokenCount,
1814
+ conversationTokens: diagnostics.recomputed.messagesTokenCount,
1815
+ toolDefinitionsTokens: contextInfo.toolDefinitionsTokens,
1816
+ };
1817
+ this.parentUsage = usage;
1818
+ this.updateAdaptiveCompactionPolicy(usage, completeDiagnostics);
1819
+ this.logger.info('Measured Copilot parent context', {
1820
+ reason,
1821
+ ...usage,
1822
+ successfulCompactions: diagnostics.attribution?.compactions.count,
1823
+ fixedPromptOverhead: diagnostics.fixedPromptOverhead,
1824
+ contextAttribution: diagnostics.attribution,
1825
+ heaviestMessages: diagnostics.heaviestMessages,
1826
+ recomputedContextTokens: diagnostics.recomputed,
1827
+ usageMetrics: diagnostics.usageMetrics,
1828
+ ...this.adaptivePolicyFields(),
1829
+ });
1830
+ return usage;
1831
+ }
1832
+ catch (err) {
1833
+ this.logger.warn('Failed to measure Copilot parent context', {
1834
+ reason,
1835
+ error: err instanceof Error ? err.message : String(err),
1836
+ previousCurrentTokens: previous?.currentTokens,
1837
+ });
1838
+ return undefined;
1839
+ }
1840
+ }
1841
+ shouldCaptureRequestDiagnostics(purpose) {
1842
+ if (purpose === 'compaction')
1843
+ return false;
1844
+ const usage = this.parentUsage;
1845
+ if (!usage)
1846
+ return true;
1847
+ const contextInfoThreshold = this.config.compactionMode === 'aggressive'
1848
+ ? usage.tokenLimit * 0.60
1849
+ : usage.tokenLimit * 0.80;
1850
+ return usage.currentTokens >= contextInfoThreshold;
1851
+ }
1852
+ async refreshParentUsageBeforeRequest(sdkSession, purpose) {
1853
+ const usage = await this.measureParentUsage(sdkSession, 'pre-request');
1854
+ if (!usage)
1855
+ return undefined;
1856
+ const previousRequestTokens = this.previousParentRequestTokens;
1857
+ const observedInterRequestGrowth = previousRequestTokens === undefined
1858
+ ? 0
1859
+ : Math.max(0, usage.currentTokens - previousRequestTokens);
1860
+ if (observedInterRequestGrowth > this.largestObservedInterRequestGrowth) {
1861
+ this.largestObservedInterRequestGrowth = observedInterRequestGrowth;
1862
+ if (this.latestContextDiagnostics) {
1863
+ this.updateAdaptiveCompactionPolicy(usage);
1864
+ }
1865
+ }
1866
+ this.previousParentRequestTokens = usage.currentTokens;
1867
+ this.logger.info('Refreshed Copilot parent context before model request', {
1868
+ purpose,
1869
+ ...usage,
1870
+ previousRequestTokens,
1871
+ ...(previousRequestTokens === undefined ? {} : { observedInterRequestGrowth }),
1872
+ ...this.adaptivePolicyFields(),
1873
+ });
1874
+ return { usage, observedInterRequestGrowth };
1875
+ }
1876
+ updateAdaptiveCompactionPolicy(usage, diagnostics = this.latestContextDiagnostics) {
1877
+ const reportedLimit = this.reportedPromptTokenLimit;
1878
+ const tokenLimit = reportedLimit === undefined
1879
+ ? usage.tokenLimit
1880
+ : Math.min(reportedLimit, usage.tokenLimit);
1881
+ if (!isPositiveFiniteNumber(tokenLimit)) {
1882
+ this.adaptiveCompactionPolicy = undefined;
1883
+ return;
1884
+ }
1885
+ // Bootstrap has no tuned token seed. Prefer the SDK's source attribution so
1886
+ // fixed context costs are separated from conversation growth. Before that
1887
+ // diagnostic surface initializes, reserve the first exact recomputation.
1888
+ const fixedPromptOverhead = diagnostics?.fixedPromptOverhead ?? 0;
1889
+ const existingPolicy = this.adaptiveCompactionPolicy;
1890
+ const bootstrapSource = existingPolicy?.bootstrapSource
1891
+ ?? (fixedPromptOverhead > 0 ? 'context-attribution' : 'first-recomputed-request');
1892
+ const initialBootstrapHeadroom = fixedPromptOverhead > 0
1893
+ ? fixedPromptOverhead
1894
+ : diagnostics?.recomputed.totalTokens ?? usage.currentTokens;
1895
+ const bootstrapHeadroom = Math.max(existingPolicy?.bootstrapHeadroom ?? 0, initialBootstrapHeadroom);
1896
+ const headroom = bootstrapHeadroom + this.largestObservedInterRequestGrowth;
1897
+ this.adaptiveCompactionPolicy = {
1898
+ tokenLimit,
1899
+ threshold: Math.max(0, tokenLimit - headroom),
1900
+ headroom,
1901
+ bootstrapHeadroom,
1902
+ bootstrapSource,
1903
+ largestObservedInterRequestGrowth: this.largestObservedInterRequestGrowth,
1904
+ };
1905
+ }
1906
+ adaptivePolicyFields() {
1907
+ const policy = this.adaptiveCompactionPolicy;
1908
+ return {
1909
+ reportedPromptTokenLimit: this.reportedPromptTokenLimit,
1910
+ reportedPromptTokenLimitSource: this.reportedPromptTokenLimitSource,
1911
+ adaptiveCompactionThreshold: policy?.threshold,
1912
+ adaptiveCompactionHeadroom: policy?.headroom,
1913
+ adaptiveBootstrapHeadroom: policy?.bootstrapHeadroom,
1914
+ adaptiveBootstrapSource: policy?.bootstrapSource,
1915
+ largestObservedInterRequestGrowth: policy?.largestObservedInterRequestGrowth
1916
+ ?? this.largestObservedInterRequestGrowth,
1917
+ };
1918
+ }
1919
+ logModelRequest(telemetry) {
1920
+ const fields = {
1921
+ ...telemetry,
1922
+ ...this.adaptivePolicyFields(),
1923
+ };
1924
+ this.logger.info('Copilot model request dispatch', fields);
1925
+ try {
1926
+ process.stderr.write(`[SdkBackend] MODEL REQUEST requestId=${telemetry.requestId} sessionId=${telemetry.sessionId ?? 'unknown'} agentId=${telemetry.agentId ?? 'parent'} parentAgentId=${telemetry.parentAgentId ?? 'none'} purpose=${telemetry.purpose} interactionType=${telemetry.interactionType ?? 'unknown'} currentTokens=${telemetry.currentTokens ?? 'unknown'} tokenLimit=${telemetry.tokenLimit ?? 'unknown'} messagesLength=${telemetry.messagesLength ?? 'unknown'} systemTokens=${telemetry.systemTokens ?? 'unknown'} conversationTokens=${telemetry.conversationTokens ?? 'unknown'} toolDefinitionsTokens=${telemetry.toolDefinitionsTokens ?? 'unknown'} adaptiveHeadroom=${this.adaptiveCompactionPolicy?.headroom ?? 'unknown'} largestInterRequestGrowth=${this.largestObservedInterRequestGrowth} observedRequestBodyBytes=${telemetry.observedRequestBodyBytes ?? 'unknown'} observedRequestBodyTokens=${telemetry.observedRequestBodyTokens ?? 'unavailable'} observedRequestBodySha256=${telemetry.observedRequestBodySha256 ?? 'unknown'}\n`);
1927
+ }
1928
+ catch { /* closed stream */ }
1929
+ }
1930
+ handleContextUsage(sdkSession, e) {
1931
+ const usage = parseContextUsage(e.data);
1932
+ if (!usage) {
1933
+ this.logger.warn('Copilot context usage event was missing valid accounting fields', {
1934
+ agentId: e.agentId,
1935
+ data: e.data,
1936
+ });
1937
+ return;
1938
+ }
1939
+ if (e.agentId) {
1940
+ this.childUsage.set(e.agentId, usage);
1941
+ }
1942
+ else {
1943
+ this.parentUsage = usage;
1944
+ if (this.latestContextDiagnostics) {
1945
+ this.updateAdaptiveCompactionPolicy(usage);
1946
+ }
1947
+ }
1948
+ const fields = {
1949
+ agentId: e.agentId,
1950
+ ...usage,
1951
+ ...(!e.agentId ? this.adaptivePolicyFields() : {}),
1952
+ };
1953
+ const scope = e.agentId ? `subagent:${e.agentId}` : 'parent';
1954
+ this.logger.info(e.agentId ? 'Copilot sub-agent context usage' : 'Copilot parent context usage', fields);
1955
+ try {
1956
+ process.stderr.write(`[SdkBackend] CONTEXT USAGE scope=${scope} currentTokens=${usage.currentTokens} tokenLimit=${usage.tokenLimit} messagesLength=${usage.messagesLength} systemTokens=${usage.systemTokens ?? 'unknown'} conversationTokens=${usage.conversationTokens ?? 'unknown'} toolDefinitionsTokens=${usage.toolDefinitionsTokens ?? 'unknown'} ceiling=${!e.agentId ? this.adaptiveCompactionPolicy?.threshold ?? 'unknown' : 'isolated'} headroom=${!e.agentId ? this.adaptiveCompactionPolicy?.headroom ?? 'unknown' : 'isolated'}\n`);
1957
+ }
1958
+ catch { /* closed stream */ }
1959
+ if (e.agentId)
1960
+ return;
1961
+ const policy = this.adaptiveCompactionPolicy;
1962
+ if (this.config.compactionMode === 'adaptive'
1963
+ && sdkSession
1964
+ && policy
1965
+ && usage.currentTokens >= policy.threshold) {
1966
+ void this.ensureCompactionHeadroom(sdkSession, 'usage-threshold');
1967
+ }
1968
+ }
1969
+ ensureCompactionHeadroom(sdkSession, source) {
1970
+ if (this.proactiveCompactionPromise)
1971
+ return this.proactiveCompactionPromise;
1972
+ const policy = this.adaptiveCompactionPolicy;
1973
+ const usage = this.parentUsage;
1974
+ if (policy === undefined
1975
+ || usage === undefined
1976
+ || usage.currentTokens < policy.threshold) {
1977
+ return Promise.resolve(true);
1978
+ }
1979
+ const compaction = this.compactForHeadroom(sdkSession, source, usage, policy);
1980
+ this.proactiveCompactionPromise = compaction;
1981
+ void compaction.then(() => {
1982
+ if (this.proactiveCompactionPromise === compaction) {
1983
+ this.proactiveCompactionPromise = undefined;
1984
+ }
1985
+ });
1986
+ return compaction;
1987
+ }
1988
+ async compactForHeadroom(sdkSession, source, before, policy) {
1989
+ if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
1990
+ return false;
1991
+ if (before.messagesLength < MIN_MESSAGES_FOR_COMPACTION) {
1992
+ return this.failUnsafeContext(source, before, policy, 0, `only ${before.messagesLength} messages are present; the CLI requires ${MIN_MESSAGES_FOR_COMPACTION}, so the latest oversized context addition cannot be compacted`);
1993
+ }
1994
+ this.logger.warn('Copilot parent context crossed the adaptive compaction ceiling', {
1995
+ source,
1996
+ ...before,
1997
+ ...this.adaptivePolicyFields(),
1998
+ });
1999
+ try {
2000
+ const beforeDiagnostics = await this.captureContextDiagnostics(sdkSession, 'pre-compaction');
2001
+ const result = await sdkSession.rpc.history.compact();
2002
+ if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
2003
+ return false;
2004
+ const afterUsage = await this.measureParentUsage(sdkSession, 'post-compaction');
2005
+ const afterDiagnostics = this.latestContextDiagnostics;
2006
+ if (!result.success || !beforeDiagnostics || !afterUsage || !afterDiagnostics) {
2007
+ return this.failUnsafeContext(source, afterUsage ?? before, policy, result.tokensRemoved, result.success
2008
+ ? 'the compaction result could not be verified with exact token recomputation and attribution counts'
2009
+ : 'the history compaction RPC reported failure');
2010
+ }
2011
+ const beforeCompactions = beforeDiagnostics.attribution?.compactions.count;
2012
+ const afterCompactions = afterDiagnostics.attribution?.compactions.count;
2013
+ const afterPolicy = this.adaptiveCompactionPolicy ?? policy;
2014
+ this.logger.info('Verified Copilot parent context compaction', {
2015
+ source,
2016
+ beforeTokens: beforeDiagnostics.recomputed.totalTokens,
2017
+ afterTokens: afterDiagnostics.recomputed.totalTokens,
2018
+ beforeSuccessfulCompactions: beforeCompactions,
2019
+ afterSuccessfulCompactions: afterCompactions,
2020
+ tokensRemoved: result.tokensRemoved,
2021
+ messagesRemoved: result.messagesRemoved,
2022
+ messagesLength: afterUsage.messagesLength,
2023
+ contextAttribution: afterDiagnostics.attribution,
2024
+ heaviestMessages: afterDiagnostics.heaviestMessages,
2025
+ usageMetrics: afterDiagnostics.usageMetrics,
2026
+ ...this.adaptivePolicyFields(),
2027
+ });
2028
+ if (beforeCompactions === undefined
2029
+ || afterCompactions === undefined
2030
+ || afterCompactions <= beforeCompactions) {
2031
+ return this.failUnsafeContext(source, afterUsage, afterPolicy, result.tokensRemoved, 'the successful compaction count did not advance');
2032
+ }
2033
+ if (afterDiagnostics.recomputed.totalTokens >= beforeDiagnostics.recomputed.totalTokens) {
2034
+ return this.failUnsafeContext(source, afterUsage, afterPolicy, result.tokensRemoved, 'exact token recomputation did not decrease after compaction; automatic truncation is unsafe without a caller-approved history boundary');
2035
+ }
2036
+ if (afterUsage.currentTokens >= afterPolicy.threshold) {
2037
+ return this.failUnsafeContext(source, afterUsage, afterPolicy, result.tokensRemoved, 'verified compaction reduced the context but did not restore the required adaptive headroom; automatic truncation is unsafe without a caller-approved history boundary');
2038
+ }
2039
+ return true;
2040
+ }
2041
+ catch (err) {
2042
+ if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
2043
+ return false;
2044
+ return this.failUnsafeContext(source, before, policy, 0, `history compaction RPC failed: ${err instanceof Error ? err.message : String(err)}`);
2045
+ }
2046
+ }
2047
+ async verifyCompletedCompaction(sdkSession, data, generation, baselinePromise) {
2048
+ const beforeUsage = this.parentUsage;
2049
+ const beforeDiagnostics = await baselinePromise;
2050
+ const after = await this.measureParentUsage(sdkSession, 'post-compaction');
2051
+ if (this.aborted
2052
+ || this.turnSettled
2053
+ || this._sdkSession !== sdkSession
2054
+ || generation !== this.parentCompactionGeneration)
2055
+ return;
2056
+ const afterDiagnostics = this.latestContextDiagnostics;
2057
+ const policy = this.adaptiveCompactionPolicy;
2058
+ const beforeCompactions = beforeDiagnostics?.attribution?.compactions.count;
2059
+ const afterCompactions = afterDiagnostics?.attribution?.compactions.count;
2060
+ if (!beforeUsage
2061
+ || !after
2062
+ || !beforeDiagnostics
2063
+ || !afterDiagnostics
2064
+ || beforeCompactions === undefined
2065
+ || afterCompactions === undefined) {
2066
+ const usage = after ?? beforeUsage;
2067
+ if (usage) {
2068
+ this.failUnsafeContext('compaction-complete', usage, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'successful compaction could not be verified with exact token recomputation and attribution counts');
2069
+ }
2070
+ else {
2071
+ this.fail(new Error('Successful compaction could not be re-measured'), 'compaction.verification_failure', data);
2072
+ }
2073
+ return;
2074
+ }
2075
+ if (afterCompactions <= beforeCompactions) {
2076
+ this.failUnsafeContext('compaction-complete', after, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'the successful compaction count did not advance');
2077
+ return;
2078
+ }
2079
+ if (afterDiagnostics.recomputed.totalTokens >= beforeDiagnostics.recomputed.totalTokens) {
2080
+ this.failUnsafeContext('compaction-complete', after, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'exact token recomputation did not decrease after compaction');
2081
+ return;
2082
+ }
2083
+ if (this.config.compactionMode === 'adaptive'
2084
+ && policy
2085
+ && after.currentTokens >= policy.threshold) {
2086
+ this.failUnsafeContext('compaction-complete', after, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'successful compaction did not restore the required adaptive headroom');
2087
+ return;
2088
+ }
2089
+ const pre = nonNegativeFiniteNumber(data?.preCompactionTokens);
2090
+ const post = nonNegativeFiniteNumber(data?.postCompactionTokens);
2091
+ const removed = nonNegativeFiniteNumber(data?.tokensRemoved);
2092
+ this.logger.info('Verified completed Copilot parent compaction', {
2093
+ eventPreCompactionTokens: pre,
2094
+ eventPostCompactionTokens: post,
2095
+ measuredPreCompactionTokens: beforeDiagnostics.recomputed.totalTokens,
2096
+ measuredPostCompactionTokens: afterDiagnostics.recomputed.totalTokens,
2097
+ beforeSuccessfulCompactions: beforeCompactions,
2098
+ afterSuccessfulCompactions: afterCompactions,
2099
+ tokensRemoved: removed,
2100
+ contextAttribution: afterDiagnostics.attribution,
2101
+ heaviestMessages: afterDiagnostics.heaviestMessages,
2102
+ ...this.adaptivePolicyFields(),
2103
+ });
2104
+ const summary = `${beforeDiagnostics.recomputed.totalTokens} → ${afterDiagnostics.recomputed.totalTokens} exact tokens (compactions ${beforeCompactions} → ${afterCompactions})`;
2105
+ this.verifiedParentCompactionGeneration = generation;
2106
+ this.emit('compaction', 'complete', summary);
2107
+ }
2108
+ failUnsafeContext(source, usage, policy, tokensRemoved, reason) {
2109
+ const fields = {
2110
+ source,
2111
+ currentTokens: usage.currentTokens,
2112
+ tokenLimit: policy?.tokenLimit ?? usage.tokenLimit,
2113
+ adaptiveCompactionThreshold: policy?.threshold,
2114
+ adaptiveCompactionHeadroom: policy?.headroom,
2115
+ adaptiveBootstrapHeadroom: policy?.bootstrapHeadroom,
2116
+ adaptiveBootstrapSource: policy?.bootstrapSource,
2117
+ largestObservedInterRequestGrowth: policy?.largestObservedInterRequestGrowth
2118
+ ?? this.largestObservedInterRequestGrowth,
2119
+ tokensRemoved,
2120
+ messagesLength: usage.messagesLength,
2121
+ systemTokens: usage.systemTokens,
2122
+ conversationTokens: usage.conversationTokens,
2123
+ toolDefinitionsTokens: usage.toolDefinitionsTokens,
2124
+ reason,
2125
+ };
2126
+ const message = `Unsafe parent context: currentTokens=${usage.currentTokens}, tokenLimit=${fields.tokenLimit}, ceiling=${policy?.threshold ?? 'unknown'}, headroom=${policy?.headroom ?? 'unknown'}, tokensRemoved=${tokensRemoved}, messagesLength=${usage.messagesLength}; ${reason}`;
2127
+ this.logger.error('Copilot parent context could not be compacted safely', fields);
2128
+ try {
2129
+ process.stderr.write(`[SdkBackend] ADAPTIVE COMPACTION TERMINAL: ${message}\n`);
2130
+ }
2131
+ catch { /* closed stream */ }
2132
+ this.fail(new Error(message), 'adaptive-compaction', fields);
2133
+ return false;
2134
+ }
1353
2135
  async resolvePendingRequest(sdkSession, e) {
1354
2136
  if (this.aborted || this._sdkSession !== sdkSession)
1355
2137
  return;
@@ -1417,7 +2199,10 @@ class SdkSession {
1417
2199
  this.abortGraceTimer = null;
1418
2200
  }
1419
2201
  this.compactionInProgress = false;
1420
- this.compactionRecoveryInProgress = false;
2202
+ this.parentCompactionGeneration = 0;
2203
+ this.verifiedParentCompactionGeneration = 0;
2204
+ this.compactionBaselinePromise = undefined;
2205
+ this.proactiveCompactionPromise = undefined;
1421
2206
  }
1422
2207
  }
1423
2208
  //# sourceMappingURL=sdk-backend.js.map