condukt 0.16.8 → 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 (34) 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 +845 -115
  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 +5 -5
  21. package/dist/src/agent.d.ts.map +1 -1
  22. package/dist/src/agent.js +20 -15
  23. package/dist/src/agent.js.map +1 -1
  24. package/dist/src/index.d.ts +1 -1
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/panel-node.d.ts +4 -2
  28. package/dist/src/panel-node.d.ts.map +1 -1
  29. package/dist/src/panel-node.js +4 -0
  30. package/dist/src/panel-node.js.map +1 -1
  31. package/dist/src/types.d.ts +92 -4
  32. package/dist/src/types.d.ts.map +1 -1
  33. package/dist/src/types.js.map +1 -1
  34. 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,22 +1162,33 @@ 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
- if (!e.agentId)
889
- this.settleIdle();
1168
+ if (e.agentId)
1169
+ return;
1170
+ if (e.data?.success === false) {
1171
+ const fields = {
1172
+ eventType: e.type ?? 'session.task_complete',
1173
+ ...(typeof e.data.summary === 'string' ? { summary: e.data.summary } : {}),
1174
+ };
1175
+ this.logger.warn('Copilot task completion failed; session remains active', fields);
1176
+ try {
1177
+ process.stderr.write('[SdkBackend] TASK COMPLETION FAILED; session remains active\n');
1178
+ }
1179
+ catch { /* closed stream */ }
1180
+ return;
1181
+ }
1182
+ this.settleIdle();
890
1183
  });
891
1184
  // The CLI normally follows an abort event with session.idle. If that
892
1185
  // terminal event is lost, fail quickly rather than waiting for heartbeat.
893
1186
  sdkSession.on('abort', (e) => {
894
- if (!isActive() || this.compactionRecoveryInProgress)
1187
+ if (!isActive())
895
1188
  return;
896
1189
  this.resetHeartbeat();
897
1190
  if (this.logAgentScopedFailure(e))
898
1191
  return;
899
- if (this.intentionalAborts.delete(sdkSession))
900
- return;
901
1192
  if (this.abortGraceTimer)
902
1193
  clearTimeout(this.abortGraceTimer);
903
1194
  this.abortGraceTimer = setTimeout(() => {
@@ -950,7 +1241,34 @@ class SdkSession {
950
1241
  Object.assign(error, { statusCode });
951
1242
  if (errorCode !== undefined)
952
1243
  Object.assign(error, { errorCode });
953
- 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;
954
1272
  });
955
1273
  // --- Context compaction (infinite sessions) ---
956
1274
  // During compaction the model goes silent. SUSPEND the heartbeat entirely
@@ -958,10 +1276,12 @@ class SdkSession {
958
1276
  sdkSession.on('session.compaction_start', (e) => {
959
1277
  if (!isActive())
960
1278
  return;
961
- this.resetHeartbeat();
962
1279
  if (e.agentId)
963
1280
  return;
1281
+ this.resetHeartbeat();
964
1282
  this.compactionInProgress = true;
1283
+ this.parentCompactionGeneration += 1;
1284
+ this.compactionBaselinePromise = this.captureContextDiagnostics(sdkSession, 'pre-compaction');
965
1285
  // SUSPEND heartbeat — compaction silence is expected
966
1286
  if (this.heartbeatTimer) {
967
1287
  clearTimeout(this.heartbeatTimer);
@@ -992,82 +1312,71 @@ class SdkSession {
992
1312
  return;
993
1313
  // If force-compact works, compaction_complete will fire naturally
994
1314
  }
995
- catch {
996
- // Re-check guards after await
1315
+ catch (err) {
997
1316
  if (!isActive())
998
1317
  return;
999
- try {
1000
- process.stderr.write('[SdkBackend] Force compact failed — aborting + re-sending\n');
1001
- }
1002
- catch { /* */ }
1003
- // Escalate: abort current message, then re-send to nudge model.
1004
- // Uses RAW SDK abort (session stays alive), NOT this.abort() (which tears down).
1005
- // SDK docs: "The session remains valid and can continue to be used for new messages."
1006
- // Wait for idle after abort before sending, to avoid racing with in-flight processing.
1007
- this.compactionInProgress = false;
1008
- this.compactionRecoveryInProgress = true;
1009
- this._turnText.clear();
1010
- try {
1011
- this.intentionalAborts.add(session);
1012
- await session.abort();
1013
- // Some SDK versions emit abort synchronously, others just after the
1014
- // request resolves. Keep the marker through the settling window.
1015
- if (this.abortGraceTimer) {
1016
- clearTimeout(this.abortGraceTimer);
1017
- this.abortGraceTimer = null;
1018
- }
1019
- // WeakSet entries are removed when the delayed intentional abort event
1020
- // arrives. The handle itself becomes collectible after session cleanup.
1021
- // Wait briefly for the abort to settle before re-sending.
1022
- // The SDK resolves abort() on acknowledgement, not on idle.
1023
- await new Promise(resolve => setTimeout(resolve, 2000));
1024
- if (!isActive())
1025
- return;
1026
- await session.send({ prompt: 'Continue from where you left off.' });
1027
- this.compactionRecoveryInProgress = false;
1028
- this.resetHeartbeat();
1029
- }
1030
- catch {
1031
- this.compactionRecoveryInProgress = false;
1032
- try {
1033
- process.stderr.write('[SdkBackend] Recovery failed — restarting heartbeat as safety net\n');
1034
- }
1035
- catch { /* */ }
1036
- this.resetHeartbeat(); // Detect if model is truly dead instead of waiting for hours-long hard timeout
1037
- }
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);
1038
1335
  }
1039
1336
  }, 3 * 60 * 1000);
1040
1337
  });
1041
1338
  sdkSession.on('session.compaction_complete', (e) => {
1042
1339
  if (!isActive())
1043
1340
  return;
1044
- if (e.agentId) {
1045
- this.resetHeartbeat();
1341
+ if (e.agentId)
1046
1342
  return;
1047
- }
1048
1343
  this.compactionInProgress = false;
1049
1344
  if (this.compactionTimer) {
1050
1345
  clearTimeout(this.compactionTimer);
1051
1346
  this.compactionTimer = null;
1052
1347
  }
1053
- // 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.
1054
1350
  this.resetHeartbeat();
1055
1351
  const data = e.data;
1056
- // Check if compaction actually succeeded
1057
1352
  if (data?.success === false) {
1058
1353
  const errMsg = typeof data.error === 'string' ? data.error : 'unknown reason';
1059
- try {
1060
- process.stderr.write(`[SdkBackend] Compaction failed: ${errMsg}\n`);
1061
- }
1062
- 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;
1063
1375
  }
1064
- const pre = data?.preCompactionTokens;
1065
- const post = data?.postCompactionTokens;
1066
- const removed = data?.tokensRemoved;
1067
- const summary = pre && post
1068
- ? `${pre} → ${post} tokens${removed != null ? ` (saved ${removed})` : ''}`
1069
- : '';
1070
- 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);
1071
1380
  });
1072
1381
  // --- Subagent lifecycle ---
1073
1382
  // Sub-agents use their own event path (subagent_start/subagent_end).
@@ -1088,7 +1397,18 @@ class SdkSession {
1088
1397
  this.resetHeartbeat();
1089
1398
  const data = e.data;
1090
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);
1091
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);
1092
1412
  this.emit('subagent_end', name, { ...data, toolCallId });
1093
1413
  });
1094
1414
  sdkSession.on('subagent.failed', (e) => {
@@ -1252,6 +1572,10 @@ class SdkSession {
1252
1572
  // Only the pre-handle creation window is handled here.
1253
1573
  if (this._sdkSession)
1254
1574
  return;
1575
+ if (e.type === 'session.usage_info') {
1576
+ this.handleContextUsage(undefined, e);
1577
+ return;
1578
+ }
1255
1579
  const eventClass = (0, lifecycle_events_1.classifySdkEvent)(e.type);
1256
1580
  if (eventClass === 'informational')
1257
1581
  return;
@@ -1337,6 +1661,409 @@ class SdkSession {
1337
1661
  || typeof data.error === 'string'
1338
1662
  || (data.error != null && typeof data.error === 'object');
1339
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
+ }
1340
2067
  async resolvePendingRequest(sdkSession, e) {
1341
2068
  if (this.aborted || this._sdkSession !== sdkSession)
1342
2069
  return;
@@ -1404,7 +2131,10 @@ class SdkSession {
1404
2131
  this.abortGraceTimer = null;
1405
2132
  }
1406
2133
  this.compactionInProgress = false;
1407
- this.compactionRecoveryInProgress = false;
2134
+ this.parentCompactionGeneration = 0;
2135
+ this.verifiedParentCompactionGeneration = 0;
2136
+ this.compactionBaselinePromise = undefined;
2137
+ this.proactiveCompactionPromise = undefined;
1408
2138
  }
1409
2139
  }
1410
2140
  //# sourceMappingURL=sdk-backend.js.map