condukt 0.16.9 → 0.16.10

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 (33) 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 +2 -2
  7. package/dist/runtimes/copilot/index.d.ts.map +1 -1
  8. package/dist/runtimes/copilot/index.js.map +1 -1
  9. package/dist/runtimes/copilot/sdk-backend.d.ts +5 -2
  10. package/dist/runtimes/copilot/sdk-backend.d.ts.map +1 -1
  11. package/dist/runtimes/copilot/sdk-backend.js +830 -113
  12. package/dist/runtimes/copilot/sdk-backend.js.map +1 -1
  13. package/dist/runtimes/copilot/subagents.d.ts +17 -0
  14. package/dist/runtimes/copilot/subagents.d.ts.map +1 -1
  15. package/dist/runtimes/copilot/subagents.js.map +1 -1
  16. package/dist/src/agent-node.d.ts +3 -2
  17. package/dist/src/agent-node.d.ts.map +1 -1
  18. package/dist/src/agent-node.js +4 -0
  19. package/dist/src/agent-node.js.map +1 -1
  20. package/dist/src/agent.d.ts.map +1 -1
  21. package/dist/src/agent.js +4 -0
  22. package/dist/src/agent.js.map +1 -1
  23. package/dist/src/index.d.ts +1 -1
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/panel-node.d.ts +4 -2
  27. package/dist/src/panel-node.d.ts.map +1 -1
  28. package/dist/src/panel-node.js +4 -0
  29. package/dist/src/panel-node.js.map +1 -1
  30. package/dist/src/types.d.ts +92 -4
  31. package/dist/src/types.d.ts.map +1 -1
  32. package/dist/src/types.js.map +1 -1
  33. 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,9 @@ class SdkBackend {
336
471
  mcpConfigPath;
337
472
  configDirectory;
338
473
  subagentRoster;
474
+ subagentsEnabled;
475
+ maxDepth;
476
+ maxConcurrency;
339
477
  extraPathDirs;
340
478
  pathTools;
341
479
  logger;
@@ -345,6 +483,9 @@ class SdkBackend {
345
483
  this.mcpConfigPath = options.mcpConfigPath;
346
484
  this.configDirectory = options.configDir;
347
485
  this.subagentRoster = options.subagentRoster;
486
+ this.subagentsEnabled = options.subagentsEnabled ?? true;
487
+ this.maxDepth = options.maxDepth;
488
+ this.maxConcurrency = options.maxConcurrency;
348
489
  this.extraPathDirs = options.extraPathDirs ?? [];
349
490
  this.pathTools = options.pathTools ?? [];
350
491
  this.logger = options.logger ?? types_1.NO_OP_LOGGER;
@@ -366,7 +507,7 @@ class SdkBackend {
366
507
  if (cached)
367
508
  return cached;
368
509
  const resolution = this.resolveModelCapabilitiesUncached(client, model).catch((err) => {
369
- this.logger.warn('Copilot model capability resolution failed; using proportional compaction thresholds only', {
510
+ this.logger.warn('Copilot model capability resolution failed; awaiting runtime context accounting', {
370
511
  model,
371
512
  reason: 'capability_resolution_failed',
372
513
  error: err instanceof Error ? err.message : String(err),
@@ -382,7 +523,7 @@ class SdkBackend {
382
523
  return {};
383
524
  const selected = models.find(candidate => candidate.id === model);
384
525
  if (!selected) {
385
- this.logger.warn('Copilot model capability lookup failed; using proportional compaction thresholds only', {
526
+ this.logger.warn('Copilot model capability lookup failed; awaiting runtime context accounting', {
386
527
  model,
387
528
  reason: 'model_not_found',
388
529
  });
@@ -402,7 +543,7 @@ class SdkBackend {
402
543
  ? contextWindowLimit
403
544
  : undefined;
404
545
  if (source === undefined || discoveredLimit === undefined) {
405
- this.logger.warn('Copilot model capability lookup returned no usable token limit; using proportional compaction thresholds only', {
546
+ this.logger.warn('Copilot model capability lookup returned no usable token limit; awaiting runtime context accounting', {
406
547
  model,
407
548
  reason: 'invalid_limit',
408
549
  maxPromptTokens: promptLimit,
@@ -410,28 +551,14 @@ class SdkBackend {
410
551
  });
411
552
  return {};
412
553
  }
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', {
554
+ this.logger.info('Resolved Copilot model prompt-token limit', {
424
555
  model,
425
- discoveredLimit,
556
+ reportedPromptTokenLimit: discoveredLimit,
426
557
  limitSource: source,
427
- promptTokenCeiling,
428
558
  });
429
559
  return {
430
- modelCapabilities: {
431
- limits: {
432
- max_prompt_tokens: promptTokenCeiling,
433
- },
434
- },
560
+ reportedPromptTokenLimit: Math.floor(discoveredLimit),
561
+ limitSource: source,
435
562
  };
436
563
  }
437
564
  listModels(client) {
@@ -443,7 +570,7 @@ class SdkBackend {
443
570
  return await client.listModels();
444
571
  }
445
572
  catch (err) {
446
- this.logger.warn('Copilot model capability discovery failed; using proportional compaction thresholds only', {
573
+ this.logger.warn('Copilot model capability discovery failed; awaiting runtime context accounting', {
447
574
  reason: 'list_models_failed',
448
575
  error: err instanceof Error ? err.message : String(err),
449
576
  });
@@ -456,7 +583,7 @@ class SdkBackend {
456
583
  if (options?.signal?.aborted) {
457
584
  throw new Error('Session creation aborted');
458
585
  }
459
- return new SdkSession(config, this.mcpConfigPath, this.configDirectory, this.subagentRoster, this.extraPathDirs, this.pathTools, this.logger, (client, model) => this.resolveModelCapabilities(client, model));
586
+ return new SdkSession(config, this.mcpConfigPath, this.configDirectory, this.subagentRoster, this.subagentsEnabled, this.maxDepth, this.maxConcurrency, this.extraPathDirs, this.pathTools, this.logger, (client, model) => this.resolveModelCapabilities(client, model));
460
587
  }
461
588
  }
462
589
  exports.SdkBackend = SdkBackend;
@@ -481,6 +608,9 @@ class SdkSession {
481
608
  mcpConfigPath;
482
609
  configDirectory;
483
610
  backendRoster;
611
+ backendSubagentsEnabled;
612
+ backendMaxDepth;
613
+ backendMaxConcurrency;
484
614
  extraPathDirs;
485
615
  pathTools;
486
616
  logger;
@@ -490,11 +620,23 @@ class SdkSession {
490
620
  compactionTimer = null;
491
621
  abortGraceTimer = null;
492
622
  compactionInProgress = false;
623
+ parentCompactionGeneration = 0;
624
+ verifiedParentCompactionGeneration = 0;
625
+ compactionBaselinePromise;
626
+ proactiveCompactionPromise;
627
+ reportedPromptTokenLimit;
628
+ reportedPromptTokenLimitSource;
629
+ adaptiveCompactionPolicy;
630
+ previousParentRequestTokens;
631
+ largestObservedInterRequestGrowth = 0;
632
+ parentUsage;
633
+ latestContextDiagnostics;
634
+ childUsage = new Map();
635
+ retryingAgentIds = new Set();
636
+ retryingParent = false;
493
637
  turnSettled = false;
494
- compactionRecoveryInProgress = false;
495
638
  aborted = false;
496
639
  _turnText = new Map();
497
- intentionalAborts = new WeakSet();
498
640
  /**
499
641
  * Maps toolCallId -> toolName for attributing tool_complete events.
500
642
  * Populated from assistant.message toolRequests and tool.execution_start.
@@ -515,11 +657,56 @@ class SdkSession {
515
657
  // SDK manages the CLI process internally; no direct PID access
516
658
  return null;
517
659
  }
518
- constructor(config, mcpConfigPath, configDirectory, backendRoster, extraPathDirs, pathTools, logger, resolveModelCapabilities) {
660
+ history = {
661
+ summarizeForHandoff: async () => {
662
+ const sdkSession = this.activeSdkSession();
663
+ const result = await sdkSession.rpc.history.summarizeForHandoff();
664
+ this.logger.info('Copilot history handoff summary completed', {
665
+ summaryLength: result.summary.length,
666
+ });
667
+ return result.summary;
668
+ },
669
+ truncate: async (eventId) => {
670
+ const sdkSession = this.activeSdkSession();
671
+ const result = await sdkSession.rpc.history.truncate({ eventId });
672
+ this.logger.warn('Copilot history truncation completed', {
673
+ eventId,
674
+ eventsRemoved: result.eventsRemoved,
675
+ });
676
+ return result.eventsRemoved;
677
+ },
678
+ };
679
+ metadata = {
680
+ getContextAttribution: async () => {
681
+ const result = await this.activeSdkSession().rpc.metadata.getContextAttribution();
682
+ return result.contextAttribution ?? null;
683
+ },
684
+ getContextHeaviestMessages: async (limit) => {
685
+ return this.activeSdkSession().rpc.metadata.getContextHeaviestMessages(limit === undefined ? {} : { limit });
686
+ },
687
+ recomputeContextTokens: async (modelId = this.config.model) => {
688
+ return this.activeSdkSession().rpc.metadata.recomputeContextTokens({ modelId });
689
+ },
690
+ };
691
+ usage = {
692
+ getMetrics: async () => {
693
+ return this.activeSdkSession().rpc.usage.getMetrics();
694
+ },
695
+ };
696
+ activeSdkSession() {
697
+ const sdkSession = this._sdkSession;
698
+ if (!sdkSession)
699
+ throw new Error('SDK session is not active');
700
+ return sdkSession;
701
+ }
702
+ constructor(config, mcpConfigPath, configDirectory, backendRoster, backendSubagentsEnabled, backendMaxDepth, backendMaxConcurrency, extraPathDirs, pathTools, logger, resolveModelCapabilities) {
519
703
  this.config = config;
520
704
  this.mcpConfigPath = mcpConfigPath;
521
705
  this.configDirectory = configDirectory;
522
706
  this.backendRoster = backendRoster;
707
+ this.backendSubagentsEnabled = backendSubagentsEnabled;
708
+ this.backendMaxDepth = backendMaxDepth;
709
+ this.backendMaxConcurrency = backendMaxConcurrency;
523
710
  this.extraPathDirs = extraPathDirs;
524
711
  this.pathTools = pathTools;
525
712
  this.logger = logger;
@@ -542,6 +729,16 @@ class SdkSession {
542
729
  this._toolCallNames.clear();
543
730
  this._callIdToParent.clear();
544
731
  this._pendingPartials.clear();
732
+ this.parentUsage = undefined;
733
+ this.latestContextDiagnostics = undefined;
734
+ this.adaptiveCompactionPolicy = undefined;
735
+ this.previousParentRequestTokens = undefined;
736
+ this.largestObservedInterRequestGrowth = 0;
737
+ this.childUsage.clear();
738
+ this.retryingAgentIds.clear();
739
+ this.retryingParent = false;
740
+ this.compactionBaselinePromise = undefined;
741
+ this.proactiveCompactionPromise = undefined;
545
742
  this.turnSettled = false;
546
743
  this._run(prompt).catch((err) => {
547
744
  this.fail(err instanceof Error ? err : new Error(String(err)), 'session.run');
@@ -558,7 +755,7 @@ class SdkSession {
558
755
  const sdkModuleName = '@github/copilot-sdk';
559
756
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
560
757
  const dynamicImport = new Function('specifier', 'return import(specifier)');
561
- const { CopilotClient, RuntimeConnection, approveAll } = await dynamicImport(sdkModuleName);
758
+ const { CopilotClient, CopilotRequestHandler, CopilotWebSocketForwarder, RuntimeConnection, approveAll, } = await dynamicImport(sdkModuleName);
562
759
  // ---------------------------------------------------------------
563
760
  // Build hardened environment (strip NODE_OPTIONS, extend PATH)
564
761
  // ---------------------------------------------------------------
@@ -593,6 +790,55 @@ class SdkSession {
593
790
  connection: RuntimeConnection.forStdio(),
594
791
  env,
595
792
  logLevel: 'warning',
793
+ requestHandler: createObservedRequestHandler(CopilotRequestHandler, CopilotWebSocketForwarder, (telemetry) => this.logModelRequest(telemetry), (agentId) => agentId ? this.childUsage.get(agentId) : this.parentUsage, (purpose) => {
794
+ const sdkSession = this._sdkSession;
795
+ return sdkSession && (purpose === 'compaction'
796
+ || this.config.compactionMode === 'adaptive'
797
+ || this.shouldCaptureRequestDiagnostics(purpose))
798
+ ? this.refreshParentUsageBeforeRequest(sdkSession, purpose)
799
+ : Promise.resolve(this.parentUsage === undefined
800
+ ? undefined
801
+ : { usage: this.parentUsage, observedInterRequestGrowth: 0 });
802
+ }, (agentId, consume) => {
803
+ if (agentId) {
804
+ const retry = this.retryingAgentIds.has(agentId);
805
+ if (retry && consume)
806
+ this.retryingAgentIds.delete(agentId);
807
+ return retry;
808
+ }
809
+ const retry = this.retryingParent;
810
+ if (retry && consume)
811
+ this.retryingParent = false;
812
+ return retry;
813
+ },
814
+ // Capability discovery also uses this handler before createSession has
815
+ // returned a session handle. Scope the allowance to traffic that has no
816
+ // session provenance so a delayed createSession cannot open a parent turn.
817
+ (context, purpose, child, measurement) => {
818
+ if (this.aborted || this.turnSettled)
819
+ return false;
820
+ if (this._sdkSession === null) {
821
+ return context.sessionId === undefined && context.agentId === undefined;
822
+ }
823
+ if (child)
824
+ return true;
825
+ const usage = measurement?.usage;
826
+ if (purpose === 'compaction') {
827
+ return usage !== undefined && usage.currentTokens < usage.tokenLimit;
828
+ }
829
+ if (this.parentCompactionGeneration > this.verifiedParentCompactionGeneration
830
+ || this.compactionInProgress)
831
+ return false;
832
+ if (measurement === undefined)
833
+ return false;
834
+ const measuredUsage = measurement.usage;
835
+ if (measuredUsage.currentTokens + measurement.observedInterRequestGrowth
836
+ >= measuredUsage.tokenLimit)
837
+ return false;
838
+ if (this.config.compactionMode !== 'adaptive')
839
+ return true;
840
+ return this.canDispatchParentRequest(measuredUsage);
841
+ }),
596
842
  });
597
843
  this._client = client;
598
844
  // ---------------------------------------------------------------
@@ -617,14 +863,30 @@ class SdkSession {
617
863
  else {
618
864
  roster = this.backendRoster;
619
865
  }
620
- const { modelCapabilities } = await this.resolveModelCapabilities(client, this.config.model);
866
+ const { reportedPromptTokenLimit, limitSource, } = await this.resolveModelCapabilities(client, this.config.model);
867
+ this.reportedPromptTokenLimit = reportedPromptTokenLimit;
868
+ this.reportedPromptTokenLimitSource = limitSource;
869
+ const subagentsEnabled = this.config.subagentsEnabled ?? this.backendSubagentsEnabled;
870
+ const permissionHandler = subagentsEnabled
871
+ ? approveAll
872
+ : (request, invocation) => {
873
+ const toolName = request.kind === 'custom-tool'
874
+ || request.kind === 'mcp'
875
+ || request.kind === 'hook'
876
+ ? request.toolName
877
+ : undefined;
878
+ if (toolName === 'task') {
879
+ return {
880
+ kind: 'reject',
881
+ feedback: 'Model-issued sub-agent dispatch is disabled for this session.',
882
+ };
883
+ }
884
+ return approveAll(request, invocation);
885
+ };
621
886
  const sessionConfig = {
622
887
  model: this.config.model,
623
888
  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,
889
+ onPermissionRequest: permissionHandler,
628
890
  workingDirectory: this.config.cwd,
629
891
  reasoningEffort: this.config.thinkingBudget,
630
892
  ...(this.config.contextTier !== undefined
@@ -639,9 +901,12 @@ class SdkSession {
639
901
  ...(this.config.availableTools !== undefined
640
902
  ? { availableTools: [...this.config.availableTools] }
641
903
  : {}),
642
- ...(this.config.excludedTools !== undefined
643
- ? { excludedTools: [...this.config.excludedTools] }
644
- : {}),
904
+ ...(() => {
905
+ const excludedTools = new Set(this.config.excludedTools ?? []);
906
+ if (!subagentsEnabled)
907
+ excludedTools.add('task');
908
+ return excludedTools.size > 0 ? { excludedTools: [...excludedTools] } : {};
909
+ })(),
645
910
  ...(this.config.customAgents !== undefined
646
911
  ? { customAgents: this.config.customAgents.map(toSdkCustomAgent) }
647
912
  : {}),
@@ -661,15 +926,20 @@ class SdkSession {
661
926
  // The SDK defaults this on, so every commit an agent composes carries a
662
927
  // Co-authored-by trailer. Output should read as the repository owner's work.
663
928
  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
- },
929
+ // Stock documented settings are the default and do not override model
930
+ // capabilities. The reduced legacy thresholds remain available for
931
+ // comparative diagnostics; the measured adaptive controller is opt-in.
932
+ infiniteSessions: this.config.compactionMode === 'aggressive'
933
+ ? {
934
+ enabled: true,
935
+ backgroundCompactionThreshold: 0.60,
936
+ bufferExhaustionThreshold: 0.75,
937
+ }
938
+ : {
939
+ enabled: true,
940
+ backgroundCompactionThreshold: 0.80,
941
+ bufferExhaustionThreshold: 0.95,
942
+ },
673
943
  // Registered before createSession issues its RPC, closing the early-event
674
944
  // gap for session.start and *_loaded events.
675
945
  onEvent: (event) => this.handleEarlyEvent(normalizeSdkEvent(event)),
@@ -683,16 +953,23 @@ class SdkSession {
683
953
  return;
684
954
  }
685
955
  this._sdkSession = sdkSession;
956
+ const maxDepth = this.config.maxDepth ?? this.backendMaxDepth;
957
+ const maxConcurrency = this.config.maxConcurrency ?? this.backendMaxConcurrency;
958
+ const subagentSettings = {
959
+ ...(roster !== undefined && roster !== false ? { agents: roster } : {}),
960
+ ...(maxDepth !== undefined ? { maxDepth } : {}),
961
+ ...(maxConcurrency !== undefined ? { maxConcurrency } : {}),
962
+ };
686
963
  // Apply the live override before any prompt can dispatch a subagent.
687
964
  // This experimental RPC degrades safely if the installed CLI rejects it.
688
- if (roster !== undefined && roster !== false) {
965
+ if (Object.keys(subagentSettings).length > 0) {
689
966
  try {
690
967
  await sdkSession.rpc.tools.updateSubagentSettings({
691
- subagents: { agents: roster },
968
+ subagents: subagentSettings,
692
969
  });
693
970
  }
694
971
  catch (err) {
695
- this.logger.warn('Failed to apply Copilot subagent roster; using default settings', {
972
+ this.logger.warn('Failed to apply Copilot subagent settings; using default settings', {
696
973
  error: err instanceof Error ? err.message : String(err),
697
974
  });
698
975
  }
@@ -722,6 +999,9 @@ class SdkSession {
722
999
  // ---------------------------------------------------------------
723
1000
  // Send the prompt (fire-and-forget; events stream via handlers)
724
1001
  // ---------------------------------------------------------------
1002
+ if (this.config.compactionMode === 'adaptive'
1003
+ && !(await this.ensureCompactionHeadroom(sdkSession, 'pre-send')))
1004
+ return;
725
1005
  await sdkSession.send({ prompt });
726
1006
  }
727
1007
  /**
@@ -874,7 +1154,7 @@ class SdkSession {
874
1154
  });
875
1155
  // --- Session idle (agent finished all work) ---
876
1156
  sdkSession.on('session.idle', (e) => {
877
- if (!isActive() || this.compactionRecoveryInProgress)
1157
+ if (!isActive())
878
1158
  return;
879
1159
  this.resetHeartbeat();
880
1160
  if (!e.agentId)
@@ -882,7 +1162,7 @@ class SdkSession {
882
1162
  });
883
1163
  // --- task_complete → idle (some models fire this instead of session.idle) ---
884
1164
  sdkSession.on('session.task_complete', (e) => {
885
- if (!isActive() || this.compactionRecoveryInProgress)
1165
+ if (!isActive())
886
1166
  return;
887
1167
  this.resetHeartbeat();
888
1168
  if (e.agentId)
@@ -904,13 +1184,11 @@ class SdkSession {
904
1184
  // The CLI normally follows an abort event with session.idle. If that
905
1185
  // terminal event is lost, fail quickly rather than waiting for heartbeat.
906
1186
  sdkSession.on('abort', (e) => {
907
- if (!isActive() || this.compactionRecoveryInProgress)
1187
+ if (!isActive())
908
1188
  return;
909
1189
  this.resetHeartbeat();
910
1190
  if (this.logAgentScopedFailure(e))
911
1191
  return;
912
- if (this.intentionalAborts.delete(sdkSession))
913
- return;
914
1192
  if (this.abortGraceTimer)
915
1193
  clearTimeout(this.abortGraceTimer);
916
1194
  this.abortGraceTimer = setTimeout(() => {
@@ -963,7 +1241,34 @@ class SdkSession {
963
1241
  Object.assign(error, { statusCode });
964
1242
  if (errorCode !== undefined)
965
1243
  Object.assign(error, { errorCode });
966
- this.fail(error, 'model.call_failure', data);
1244
+ const compactionFailed = data?.initiator === 'compaction'
1245
+ || data?.source === 'compaction'
1246
+ || this.compactionInProgress
1247
+ || this.proactiveCompactionPromise !== undefined;
1248
+ this.fail(error, compactionFailed ? 'compaction.model.call_failure' : 'model.call_failure', {
1249
+ ...data,
1250
+ ...(compactionFailed ? { requestPurpose: 'compaction' } : {}),
1251
+ ...(this.parentUsage !== undefined
1252
+ ? {
1253
+ currentTokens: this.parentUsage.currentTokens,
1254
+ tokenLimit: this.parentUsage.tokenLimit,
1255
+ messagesLength: this.parentUsage.messagesLength,
1256
+ }
1257
+ : {}),
1258
+ });
1259
+ });
1260
+ sdkSession.on('session.usage_info', (e) => {
1261
+ if (!isActive())
1262
+ return;
1263
+ this.handleContextUsage(sdkSession, e);
1264
+ });
1265
+ sdkSession.on('assistant.turn_retry', (e) => {
1266
+ if (!isActive())
1267
+ return;
1268
+ if (e.agentId)
1269
+ this.retryingAgentIds.add(e.agentId);
1270
+ else
1271
+ this.retryingParent = true;
967
1272
  });
968
1273
  // --- Context compaction (infinite sessions) ---
969
1274
  // During compaction the model goes silent. SUSPEND the heartbeat entirely
@@ -971,10 +1276,12 @@ class SdkSession {
971
1276
  sdkSession.on('session.compaction_start', (e) => {
972
1277
  if (!isActive())
973
1278
  return;
974
- this.resetHeartbeat();
975
1279
  if (e.agentId)
976
1280
  return;
1281
+ this.resetHeartbeat();
977
1282
  this.compactionInProgress = true;
1283
+ this.parentCompactionGeneration += 1;
1284
+ this.compactionBaselinePromise = this.captureContextDiagnostics(sdkSession, 'pre-compaction');
978
1285
  // SUSPEND heartbeat — compaction silence is expected
979
1286
  if (this.heartbeatTimer) {
980
1287
  clearTimeout(this.heartbeatTimer);
@@ -1005,82 +1312,71 @@ class SdkSession {
1005
1312
  return;
1006
1313
  // If force-compact works, compaction_complete will fire naturally
1007
1314
  }
1008
- catch {
1009
- // Re-check guards after await
1315
+ catch (err) {
1010
1316
  if (!isActive())
1011
1317
  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
- }
1318
+ const usage = this.parentUsage;
1319
+ const detail = err instanceof Error ? err.message : String(err);
1320
+ const fields = {
1321
+ requestPurpose: 'compaction',
1322
+ error: detail,
1323
+ ...(usage !== undefined
1324
+ ? {
1325
+ currentTokens: usage.currentTokens,
1326
+ tokenLimit: usage.tokenLimit,
1327
+ messagesLength: usage.messagesLength,
1328
+ systemTokens: usage.systemTokens,
1329
+ conversationTokens: usage.conversationTokens,
1330
+ toolDefinitionsTokens: usage.toolDefinitionsTokens,
1331
+ }
1332
+ : {}),
1333
+ };
1334
+ this.fail(new Error(`Compaction recovery request failed: ${detail}`), 'compaction.recovery_failure', fields);
1051
1335
  }
1052
1336
  }, 3 * 60 * 1000);
1053
1337
  });
1054
1338
  sdkSession.on('session.compaction_complete', (e) => {
1055
1339
  if (!isActive())
1056
1340
  return;
1057
- if (e.agentId) {
1058
- this.resetHeartbeat();
1341
+ if (e.agentId)
1059
1342
  return;
1060
- }
1061
1343
  this.compactionInProgress = false;
1062
1344
  if (this.compactionTimer) {
1063
1345
  clearTimeout(this.compactionTimer);
1064
1346
  this.compactionTimer = null;
1065
1347
  }
1066
- // Restart heartbeat — model should resume producing output
1348
+ // Restart heartbeat while the asynchronous verification RPC runs. Any
1349
+ // ordinary parent request remains guarded at the request-handler seam.
1067
1350
  this.resetHeartbeat();
1068
1351
  const data = e.data;
1069
- // Check if compaction actually succeeded
1070
1352
  if (data?.success === false) {
1071
1353
  const errMsg = typeof data.error === 'string' ? data.error : 'unknown reason';
1072
- try {
1073
- process.stderr.write(`[SdkBackend] Compaction failed: ${errMsg}\n`);
1074
- }
1075
- catch { /* */ }
1354
+ const rawStatusCode = data.statusCode;
1355
+ const statusCode = typeof rawStatusCode === 'number'
1356
+ ? ` (HTTP ${rawStatusCode})`
1357
+ : '';
1358
+ const usage = this.parentUsage;
1359
+ const fields = {
1360
+ ...data,
1361
+ requestPurpose: 'compaction',
1362
+ ...(usage !== undefined
1363
+ ? {
1364
+ currentTokens: usage.currentTokens,
1365
+ tokenLimit: usage.tokenLimit,
1366
+ messagesLength: usage.messagesLength,
1367
+ systemTokens: usage.systemTokens,
1368
+ conversationTokens: usage.conversationTokens,
1369
+ toolDefinitionsTokens: usage.toolDefinitionsTokens,
1370
+ }
1371
+ : {}),
1372
+ };
1373
+ this.fail(new Error(`Compaction model call failed${statusCode}: ${errMsg}`), 'compaction.call_failure', fields);
1374
+ return;
1076
1375
  }
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);
1376
+ const generation = this.parentCompactionGeneration;
1377
+ const baselinePromise = this.compactionBaselinePromise;
1378
+ this.compactionBaselinePromise = undefined;
1379
+ void this.verifyCompletedCompaction(sdkSession, data, generation, baselinePromise);
1084
1380
  });
1085
1381
  // --- Subagent lifecycle ---
1086
1382
  // Sub-agents use their own event path (subagent_start/subagent_end).
@@ -1101,7 +1397,18 @@ class SdkSession {
1101
1397
  this.resetHeartbeat();
1102
1398
  const data = e.data;
1103
1399
  const name = String(data?.agentDisplayName ?? data?.agentName ?? 'agent');
1400
+ const agentName = String(data?.agentName ?? name);
1401
+ const model = typeof data?.model === 'string' ? data.model : undefined;
1402
+ const totalTokens = nonNegativeFiniteNumber(data?.totalTokens);
1104
1403
  const toolCallId = typeof data?.toolCallId === 'string' ? data.toolCallId : '';
1404
+ const fields = {
1405
+ agentName,
1406
+ model,
1407
+ cumulativeTokensConsumed: totalTokens,
1408
+ totalToolCalls: nonNegativeFiniteNumber(data?.totalToolCalls),
1409
+ durationMs: nonNegativeFiniteNumber(data?.durationMs),
1410
+ };
1411
+ this.logger.info('Copilot sub-agent cumulative token consumption', fields);
1105
1412
  this.emit('subagent_end', name, { ...data, toolCallId });
1106
1413
  });
1107
1414
  sdkSession.on('subagent.failed', (e) => {
@@ -1265,6 +1572,10 @@ class SdkSession {
1265
1572
  // Only the pre-handle creation window is handled here.
1266
1573
  if (this._sdkSession)
1267
1574
  return;
1575
+ if (e.type === 'session.usage_info') {
1576
+ this.handleContextUsage(undefined, e);
1577
+ return;
1578
+ }
1268
1579
  const eventClass = (0, lifecycle_events_1.classifySdkEvent)(e.type);
1269
1580
  if (eventClass === 'informational')
1270
1581
  return;
@@ -1350,6 +1661,409 @@ class SdkSession {
1350
1661
  || typeof data.error === 'string'
1351
1662
  || (data.error != null && typeof data.error === 'object');
1352
1663
  }
1664
+ canDispatchParentRequest(usage) {
1665
+ const policy = this.adaptiveCompactionPolicy;
1666
+ if (usage === undefined || policy === undefined) {
1667
+ if (usage !== undefined) {
1668
+ this.failUnsafeContext('pre-send', usage, policy, 0, 'fresh parent context accounting was unavailable at the request-handler seam');
1669
+ }
1670
+ else {
1671
+ this.fail(new Error('Blocked parent model request: fresh context accounting was unavailable'), 'pre-send-context-accounting');
1672
+ }
1673
+ return false;
1674
+ }
1675
+ if (usage.currentTokens < policy.threshold)
1676
+ return true;
1677
+ this.failUnsafeContext('pre-send', usage, policy, 0, 'the runtime attempted a parent model request before the required adaptive headroom was restored');
1678
+ return false;
1679
+ }
1680
+ async captureContextDiagnostics(sdkSession, reason) {
1681
+ try {
1682
+ const [attributionResult, heaviestMessages, recomputed, usageMetrics] = await Promise.all([
1683
+ sdkSession.rpc.metadata.getContextAttribution(),
1684
+ sdkSession.rpc.metadata.getContextHeaviestMessages({}),
1685
+ sdkSession.rpc.metadata.recomputeContextTokens({ modelId: this.config.model }),
1686
+ sdkSession.rpc.usage.getMetrics(),
1687
+ ]);
1688
+ const attribution = attributionResult.contextAttribution ?? null;
1689
+ const fixedPromptOverhead = attribution
1690
+ ? Math.max(0, attribution.totalTokens - recomputed.messagesTokenCount)
1691
+ : recomputed.systemTokenCount;
1692
+ const diagnostics = {
1693
+ attribution,
1694
+ heaviestMessages,
1695
+ recomputed,
1696
+ usageMetrics,
1697
+ fixedPromptOverhead,
1698
+ };
1699
+ this.logger.info('Captured Copilot parent context diagnostics', {
1700
+ reason,
1701
+ successfulCompactions: attribution?.compactions.count,
1702
+ fixedPromptOverhead,
1703
+ contextAttribution: attribution,
1704
+ heaviestMessages,
1705
+ recomputedContextTokens: recomputed,
1706
+ usageMetrics,
1707
+ });
1708
+ return diagnostics;
1709
+ }
1710
+ catch (err) {
1711
+ this.logger.warn('Failed to capture Copilot parent context diagnostics', {
1712
+ reason,
1713
+ error: err instanceof Error ? err.message : String(err),
1714
+ });
1715
+ return undefined;
1716
+ }
1717
+ }
1718
+ async measureParentUsage(sdkSession, reason) {
1719
+ const previous = this.parentUsage;
1720
+ try {
1721
+ const [contextResult, diagnostics] = await Promise.all([
1722
+ sdkSession.rpc.metadata.contextInfo({
1723
+ promptTokenLimit: previous?.tokenLimit ?? this.reportedPromptTokenLimit ?? 0,
1724
+ outputTokenLimit: 0,
1725
+ selectedModel: this.config.model,
1726
+ }),
1727
+ this.captureContextDiagnostics(sdkSession, reason),
1728
+ ]);
1729
+ const contextInfo = contextResult.contextInfo;
1730
+ if (!contextInfo || !diagnostics) {
1731
+ this.logger.warn('Copilot parent context measurement returned incomplete context information', {
1732
+ reason,
1733
+ previousCurrentTokens: previous?.currentTokens,
1734
+ hasContextInfo: contextInfo !== null && contextInfo !== undefined,
1735
+ hasDiagnostics: diagnostics !== undefined,
1736
+ });
1737
+ return undefined;
1738
+ }
1739
+ const completeDiagnostics = diagnostics;
1740
+ this.latestContextDiagnostics = completeDiagnostics;
1741
+ const usage = {
1742
+ currentTokens: diagnostics.attribution?.totalTokens ?? diagnostics.recomputed.totalTokens,
1743
+ tokenLimit: contextInfo.promptTokenLimit,
1744
+ messagesLength: previous?.messagesLength ?? 0,
1745
+ systemTokens: diagnostics.recomputed.systemTokenCount,
1746
+ conversationTokens: diagnostics.recomputed.messagesTokenCount,
1747
+ toolDefinitionsTokens: contextInfo.toolDefinitionsTokens,
1748
+ };
1749
+ this.parentUsage = usage;
1750
+ this.updateAdaptiveCompactionPolicy(usage, completeDiagnostics);
1751
+ this.logger.info('Measured Copilot parent context', {
1752
+ reason,
1753
+ ...usage,
1754
+ successfulCompactions: diagnostics.attribution?.compactions.count,
1755
+ fixedPromptOverhead: diagnostics.fixedPromptOverhead,
1756
+ contextAttribution: diagnostics.attribution,
1757
+ heaviestMessages: diagnostics.heaviestMessages,
1758
+ recomputedContextTokens: diagnostics.recomputed,
1759
+ usageMetrics: diagnostics.usageMetrics,
1760
+ ...this.adaptivePolicyFields(),
1761
+ });
1762
+ return usage;
1763
+ }
1764
+ catch (err) {
1765
+ this.logger.warn('Failed to measure Copilot parent context', {
1766
+ reason,
1767
+ error: err instanceof Error ? err.message : String(err),
1768
+ previousCurrentTokens: previous?.currentTokens,
1769
+ });
1770
+ return undefined;
1771
+ }
1772
+ }
1773
+ shouldCaptureRequestDiagnostics(purpose) {
1774
+ if (purpose === 'compaction')
1775
+ return false;
1776
+ const usage = this.parentUsage;
1777
+ if (!usage)
1778
+ return true;
1779
+ const contextInfoThreshold = this.config.compactionMode === 'aggressive'
1780
+ ? usage.tokenLimit * 0.60
1781
+ : usage.tokenLimit * 0.80;
1782
+ return usage.currentTokens >= contextInfoThreshold;
1783
+ }
1784
+ async refreshParentUsageBeforeRequest(sdkSession, purpose) {
1785
+ const usage = await this.measureParentUsage(sdkSession, 'pre-request');
1786
+ if (!usage)
1787
+ return undefined;
1788
+ const previousRequestTokens = this.previousParentRequestTokens;
1789
+ const observedInterRequestGrowth = previousRequestTokens === undefined
1790
+ ? 0
1791
+ : Math.max(0, usage.currentTokens - previousRequestTokens);
1792
+ if (observedInterRequestGrowth > this.largestObservedInterRequestGrowth) {
1793
+ this.largestObservedInterRequestGrowth = observedInterRequestGrowth;
1794
+ if (this.latestContextDiagnostics) {
1795
+ this.updateAdaptiveCompactionPolicy(usage);
1796
+ }
1797
+ }
1798
+ this.previousParentRequestTokens = usage.currentTokens;
1799
+ this.logger.info('Refreshed Copilot parent context before model request', {
1800
+ purpose,
1801
+ ...usage,
1802
+ previousRequestTokens,
1803
+ ...(previousRequestTokens === undefined ? {} : { observedInterRequestGrowth }),
1804
+ ...this.adaptivePolicyFields(),
1805
+ });
1806
+ return { usage, observedInterRequestGrowth };
1807
+ }
1808
+ updateAdaptiveCompactionPolicy(usage, diagnostics = this.latestContextDiagnostics) {
1809
+ const reportedLimit = this.reportedPromptTokenLimit;
1810
+ const tokenLimit = reportedLimit === undefined
1811
+ ? usage.tokenLimit
1812
+ : Math.min(reportedLimit, usage.tokenLimit);
1813
+ if (!isPositiveFiniteNumber(tokenLimit)) {
1814
+ this.adaptiveCompactionPolicy = undefined;
1815
+ return;
1816
+ }
1817
+ // Bootstrap has no tuned token seed. Prefer the SDK's source attribution so
1818
+ // fixed context costs are separated from conversation growth. Before that
1819
+ // diagnostic surface initializes, reserve the first exact recomputation.
1820
+ const fixedPromptOverhead = diagnostics?.fixedPromptOverhead ?? 0;
1821
+ const existingPolicy = this.adaptiveCompactionPolicy;
1822
+ const bootstrapSource = existingPolicy?.bootstrapSource
1823
+ ?? (fixedPromptOverhead > 0 ? 'context-attribution' : 'first-recomputed-request');
1824
+ const initialBootstrapHeadroom = fixedPromptOverhead > 0
1825
+ ? fixedPromptOverhead
1826
+ : diagnostics?.recomputed.totalTokens ?? usage.currentTokens;
1827
+ const bootstrapHeadroom = Math.max(existingPolicy?.bootstrapHeadroom ?? 0, initialBootstrapHeadroom);
1828
+ const headroom = bootstrapHeadroom + this.largestObservedInterRequestGrowth;
1829
+ this.adaptiveCompactionPolicy = {
1830
+ tokenLimit,
1831
+ threshold: Math.max(0, tokenLimit - headroom),
1832
+ headroom,
1833
+ bootstrapHeadroom,
1834
+ bootstrapSource,
1835
+ largestObservedInterRequestGrowth: this.largestObservedInterRequestGrowth,
1836
+ };
1837
+ }
1838
+ adaptivePolicyFields() {
1839
+ const policy = this.adaptiveCompactionPolicy;
1840
+ return {
1841
+ reportedPromptTokenLimit: this.reportedPromptTokenLimit,
1842
+ reportedPromptTokenLimitSource: this.reportedPromptTokenLimitSource,
1843
+ adaptiveCompactionThreshold: policy?.threshold,
1844
+ adaptiveCompactionHeadroom: policy?.headroom,
1845
+ adaptiveBootstrapHeadroom: policy?.bootstrapHeadroom,
1846
+ adaptiveBootstrapSource: policy?.bootstrapSource,
1847
+ largestObservedInterRequestGrowth: policy?.largestObservedInterRequestGrowth
1848
+ ?? this.largestObservedInterRequestGrowth,
1849
+ };
1850
+ }
1851
+ logModelRequest(telemetry) {
1852
+ const fields = {
1853
+ ...telemetry,
1854
+ ...this.adaptivePolicyFields(),
1855
+ };
1856
+ this.logger.info('Copilot model request dispatch', fields);
1857
+ try {
1858
+ 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`);
1859
+ }
1860
+ catch { /* closed stream */ }
1861
+ }
1862
+ handleContextUsage(sdkSession, e) {
1863
+ const usage = parseContextUsage(e.data);
1864
+ if (!usage) {
1865
+ this.logger.warn('Copilot context usage event was missing valid accounting fields', {
1866
+ agentId: e.agentId,
1867
+ data: e.data,
1868
+ });
1869
+ return;
1870
+ }
1871
+ if (e.agentId) {
1872
+ this.childUsage.set(e.agentId, usage);
1873
+ }
1874
+ else {
1875
+ this.parentUsage = usage;
1876
+ if (this.latestContextDiagnostics) {
1877
+ this.updateAdaptiveCompactionPolicy(usage);
1878
+ }
1879
+ }
1880
+ const fields = {
1881
+ agentId: e.agentId,
1882
+ ...usage,
1883
+ ...(!e.agentId ? this.adaptivePolicyFields() : {}),
1884
+ };
1885
+ const scope = e.agentId ? `subagent:${e.agentId}` : 'parent';
1886
+ this.logger.info(e.agentId ? 'Copilot sub-agent context usage' : 'Copilot parent context usage', fields);
1887
+ try {
1888
+ 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`);
1889
+ }
1890
+ catch { /* closed stream */ }
1891
+ if (e.agentId)
1892
+ return;
1893
+ const policy = this.adaptiveCompactionPolicy;
1894
+ if (this.config.compactionMode === 'adaptive'
1895
+ && sdkSession
1896
+ && policy
1897
+ && usage.currentTokens >= policy.threshold) {
1898
+ void this.ensureCompactionHeadroom(sdkSession, 'usage-threshold');
1899
+ }
1900
+ }
1901
+ ensureCompactionHeadroom(sdkSession, source) {
1902
+ if (this.proactiveCompactionPromise)
1903
+ return this.proactiveCompactionPromise;
1904
+ const policy = this.adaptiveCompactionPolicy;
1905
+ const usage = this.parentUsage;
1906
+ if (policy === undefined
1907
+ || usage === undefined
1908
+ || usage.currentTokens < policy.threshold) {
1909
+ return Promise.resolve(true);
1910
+ }
1911
+ const compaction = this.compactForHeadroom(sdkSession, source, usage, policy);
1912
+ this.proactiveCompactionPromise = compaction;
1913
+ void compaction.then(() => {
1914
+ if (this.proactiveCompactionPromise === compaction) {
1915
+ this.proactiveCompactionPromise = undefined;
1916
+ }
1917
+ });
1918
+ return compaction;
1919
+ }
1920
+ async compactForHeadroom(sdkSession, source, before, policy) {
1921
+ if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
1922
+ return false;
1923
+ if (before.messagesLength < MIN_MESSAGES_FOR_COMPACTION) {
1924
+ 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`);
1925
+ }
1926
+ this.logger.warn('Copilot parent context crossed the adaptive compaction ceiling', {
1927
+ source,
1928
+ ...before,
1929
+ ...this.adaptivePolicyFields(),
1930
+ });
1931
+ try {
1932
+ const beforeDiagnostics = await this.captureContextDiagnostics(sdkSession, 'pre-compaction');
1933
+ const result = await sdkSession.rpc.history.compact();
1934
+ if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
1935
+ return false;
1936
+ const afterUsage = await this.measureParentUsage(sdkSession, 'post-compaction');
1937
+ const afterDiagnostics = this.latestContextDiagnostics;
1938
+ if (!result.success || !beforeDiagnostics || !afterUsage || !afterDiagnostics) {
1939
+ return this.failUnsafeContext(source, afterUsage ?? before, policy, result.tokensRemoved, result.success
1940
+ ? 'the compaction result could not be verified with exact token recomputation and attribution counts'
1941
+ : 'the history compaction RPC reported failure');
1942
+ }
1943
+ const beforeCompactions = beforeDiagnostics.attribution?.compactions.count;
1944
+ const afterCompactions = afterDiagnostics.attribution?.compactions.count;
1945
+ const afterPolicy = this.adaptiveCompactionPolicy ?? policy;
1946
+ this.logger.info('Verified Copilot parent context compaction', {
1947
+ source,
1948
+ beforeTokens: beforeDiagnostics.recomputed.totalTokens,
1949
+ afterTokens: afterDiagnostics.recomputed.totalTokens,
1950
+ beforeSuccessfulCompactions: beforeCompactions,
1951
+ afterSuccessfulCompactions: afterCompactions,
1952
+ tokensRemoved: result.tokensRemoved,
1953
+ messagesRemoved: result.messagesRemoved,
1954
+ messagesLength: afterUsage.messagesLength,
1955
+ contextAttribution: afterDiagnostics.attribution,
1956
+ heaviestMessages: afterDiagnostics.heaviestMessages,
1957
+ usageMetrics: afterDiagnostics.usageMetrics,
1958
+ ...this.adaptivePolicyFields(),
1959
+ });
1960
+ if (beforeCompactions === undefined
1961
+ || afterCompactions === undefined
1962
+ || afterCompactions <= beforeCompactions) {
1963
+ return this.failUnsafeContext(source, afterUsage, afterPolicy, result.tokensRemoved, 'the successful compaction count did not advance');
1964
+ }
1965
+ if (afterDiagnostics.recomputed.totalTokens >= beforeDiagnostics.recomputed.totalTokens) {
1966
+ 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');
1967
+ }
1968
+ if (afterUsage.currentTokens >= afterPolicy.threshold) {
1969
+ 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');
1970
+ }
1971
+ return true;
1972
+ }
1973
+ catch (err) {
1974
+ if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
1975
+ return false;
1976
+ return this.failUnsafeContext(source, before, policy, 0, `history compaction RPC failed: ${err instanceof Error ? err.message : String(err)}`);
1977
+ }
1978
+ }
1979
+ async verifyCompletedCompaction(sdkSession, data, generation, baselinePromise) {
1980
+ const beforeUsage = this.parentUsage;
1981
+ const beforeDiagnostics = await baselinePromise;
1982
+ const after = await this.measureParentUsage(sdkSession, 'post-compaction');
1983
+ if (this.aborted
1984
+ || this.turnSettled
1985
+ || this._sdkSession !== sdkSession
1986
+ || generation !== this.parentCompactionGeneration)
1987
+ return;
1988
+ const afterDiagnostics = this.latestContextDiagnostics;
1989
+ const policy = this.adaptiveCompactionPolicy;
1990
+ const beforeCompactions = beforeDiagnostics?.attribution?.compactions.count;
1991
+ const afterCompactions = afterDiagnostics?.attribution?.compactions.count;
1992
+ if (!beforeUsage
1993
+ || !after
1994
+ || !beforeDiagnostics
1995
+ || !afterDiagnostics
1996
+ || beforeCompactions === undefined
1997
+ || afterCompactions === undefined) {
1998
+ const usage = after ?? beforeUsage;
1999
+ if (usage) {
2000
+ this.failUnsafeContext('compaction-complete', usage, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'successful compaction could not be verified with exact token recomputation and attribution counts');
2001
+ }
2002
+ else {
2003
+ this.fail(new Error('Successful compaction could not be re-measured'), 'compaction.verification_failure', data);
2004
+ }
2005
+ return;
2006
+ }
2007
+ if (afterCompactions <= beforeCompactions) {
2008
+ this.failUnsafeContext('compaction-complete', after, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'the successful compaction count did not advance');
2009
+ return;
2010
+ }
2011
+ if (afterDiagnostics.recomputed.totalTokens >= beforeDiagnostics.recomputed.totalTokens) {
2012
+ this.failUnsafeContext('compaction-complete', after, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'exact token recomputation did not decrease after compaction');
2013
+ return;
2014
+ }
2015
+ if (this.config.compactionMode === 'adaptive'
2016
+ && policy
2017
+ && after.currentTokens >= policy.threshold) {
2018
+ this.failUnsafeContext('compaction-complete', after, policy, nonNegativeFiniteNumber(data?.tokensRemoved) ?? 0, 'successful compaction did not restore the required adaptive headroom');
2019
+ return;
2020
+ }
2021
+ const pre = nonNegativeFiniteNumber(data?.preCompactionTokens);
2022
+ const post = nonNegativeFiniteNumber(data?.postCompactionTokens);
2023
+ const removed = nonNegativeFiniteNumber(data?.tokensRemoved);
2024
+ this.logger.info('Verified completed Copilot parent compaction', {
2025
+ eventPreCompactionTokens: pre,
2026
+ eventPostCompactionTokens: post,
2027
+ measuredPreCompactionTokens: beforeDiagnostics.recomputed.totalTokens,
2028
+ measuredPostCompactionTokens: afterDiagnostics.recomputed.totalTokens,
2029
+ beforeSuccessfulCompactions: beforeCompactions,
2030
+ afterSuccessfulCompactions: afterCompactions,
2031
+ tokensRemoved: removed,
2032
+ contextAttribution: afterDiagnostics.attribution,
2033
+ heaviestMessages: afterDiagnostics.heaviestMessages,
2034
+ ...this.adaptivePolicyFields(),
2035
+ });
2036
+ const summary = `${beforeDiagnostics.recomputed.totalTokens} → ${afterDiagnostics.recomputed.totalTokens} exact tokens (compactions ${beforeCompactions} → ${afterCompactions})`;
2037
+ this.verifiedParentCompactionGeneration = generation;
2038
+ this.emit('compaction', 'complete', summary);
2039
+ }
2040
+ failUnsafeContext(source, usage, policy, tokensRemoved, reason) {
2041
+ const fields = {
2042
+ source,
2043
+ currentTokens: usage.currentTokens,
2044
+ tokenLimit: policy?.tokenLimit ?? usage.tokenLimit,
2045
+ adaptiveCompactionThreshold: policy?.threshold,
2046
+ adaptiveCompactionHeadroom: policy?.headroom,
2047
+ adaptiveBootstrapHeadroom: policy?.bootstrapHeadroom,
2048
+ adaptiveBootstrapSource: policy?.bootstrapSource,
2049
+ largestObservedInterRequestGrowth: policy?.largestObservedInterRequestGrowth
2050
+ ?? this.largestObservedInterRequestGrowth,
2051
+ tokensRemoved,
2052
+ messagesLength: usage.messagesLength,
2053
+ systemTokens: usage.systemTokens,
2054
+ conversationTokens: usage.conversationTokens,
2055
+ toolDefinitionsTokens: usage.toolDefinitionsTokens,
2056
+ reason,
2057
+ };
2058
+ 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}`;
2059
+ this.logger.error('Copilot parent context could not be compacted safely', fields);
2060
+ try {
2061
+ process.stderr.write(`[SdkBackend] ADAPTIVE COMPACTION TERMINAL: ${message}\n`);
2062
+ }
2063
+ catch { /* closed stream */ }
2064
+ this.fail(new Error(message), 'adaptive-compaction', fields);
2065
+ return false;
2066
+ }
1353
2067
  async resolvePendingRequest(sdkSession, e) {
1354
2068
  if (this.aborted || this._sdkSession !== sdkSession)
1355
2069
  return;
@@ -1417,7 +2131,10 @@ class SdkSession {
1417
2131
  this.abortGraceTimer = null;
1418
2132
  }
1419
2133
  this.compactionInProgress = false;
1420
- this.compactionRecoveryInProgress = false;
2134
+ this.parentCompactionGeneration = 0;
2135
+ this.verifiedParentCompactionGeneration = 0;
2136
+ this.compactionBaselinePromise = undefined;
2137
+ this.proactiveCompactionPromise = undefined;
1421
2138
  }
1422
2139
  }
1423
2140
  //# sourceMappingURL=sdk-backend.js.map