erosolar-cli 1.7.412 → 1.7.413
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.
- package/dist/core/contextManager.d.ts +4 -2
- package/dist/core/contextManager.d.ts.map +1 -1
- package/dist/core/contextManager.js +15 -28
- package/dist/core/contextManager.js.map +1 -1
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +5 -6
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +13 -0
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +246 -71
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/ui/streamingFormatter.d.ts +4 -1
- package/dist/ui/streamingFormatter.d.ts.map +1 -1
- package/dist/ui/streamingFormatter.js +30 -9
- package/dist/ui/streamingFormatter.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { display } from '../ui/display.js';
|
|
7
7
|
import { isPlainOutputMode } from '../ui/outputMode.js';
|
|
8
8
|
import { theme } from '../ui/theme.js';
|
|
9
|
+
import { renderDivider } from '../ui/unified/layout.js';
|
|
9
10
|
import { StreamingResponseFormatter } from '../ui/streamingFormatter.js';
|
|
10
11
|
import { getContextWindowTokens } from '../core/contextWindow.js';
|
|
11
12
|
import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, maskSecret, setSecretValue, } from '../core/secretStore.js';
|
|
@@ -71,7 +72,7 @@ const BASE_SLASH_COMMANDS = getSlashCommands().map((cmd) => ({
|
|
|
71
72
|
// Load PROVIDER_LABELS from centralized schema
|
|
72
73
|
const PROVIDER_LABELS = Object.fromEntries(getProviders().map((provider) => [provider.id, provider.label]));
|
|
73
74
|
// Allow enough time for paste detection to kick in before flushing buffered lines
|
|
74
|
-
const CONTEXT_USAGE_THRESHOLD = 0.
|
|
75
|
+
const CONTEXT_USAGE_THRESHOLD = 0.97;
|
|
75
76
|
const CONTEXT_AUTOCOMPACT_PERCENT = Math.round(CONTEXT_USAGE_THRESHOLD * 100);
|
|
76
77
|
const CONTEXT_AUTOCOMPACT_FLOOR = 0.6;
|
|
77
78
|
const MIN_COMPACTION_TOKEN_SAVINGS = 200;
|
|
@@ -79,12 +80,11 @@ const MIN_COMPACTION_PERCENT_SAVINGS = 0.5;
|
|
|
79
80
|
const CONTEXT_RECENT_MESSAGE_COUNT = 12;
|
|
80
81
|
const CONTEXT_CLEANUP_CHARS_PER_CHUNK = 6000;
|
|
81
82
|
const CONTEXT_CLEANUP_MAX_OUTPUT_TOKENS = 800;
|
|
82
|
-
const CONTEXT_CLEANUP_SYSTEM_PROMPT = `
|
|
83
|
-
- Merge any prior summary with the new
|
|
84
|
-
- Capture
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
- Never call tools or run shell commands; respond with plain Markdown text only.`;
|
|
83
|
+
const CONTEXT_CLEANUP_SYSTEM_PROMPT = `Summarize earlier IDE collaboration so the agent can keep working.
|
|
84
|
+
- Merge any prior summary with the new chunk.
|
|
85
|
+
- Capture decisions, file edits/paths, tool findings, and open questions.
|
|
86
|
+
- Separate finished work from follow-ups; keep it under ~180 words with tight bullets.
|
|
87
|
+
- Respond in plain Markdown only (no tool or shell calls).`;
|
|
88
88
|
export class InteractiveShell {
|
|
89
89
|
agent = null;
|
|
90
90
|
profile;
|
|
@@ -124,6 +124,7 @@ export class InteractiveShell {
|
|
|
124
124
|
latestTokenUsage = { used: null, limit: null };
|
|
125
125
|
planApprovalBridgeRegistered = false;
|
|
126
126
|
contextCompactionInFlight = false;
|
|
127
|
+
contextCompactionLog = [];
|
|
127
128
|
lastContextWarningLevel = null;
|
|
128
129
|
sessionPreferences;
|
|
129
130
|
autosaveEnabled;
|
|
@@ -163,11 +164,14 @@ export class InteractiveShell {
|
|
|
163
164
|
streamingHeartbeatFrame = 0;
|
|
164
165
|
streamingStatusLabel = null;
|
|
165
166
|
lastStreamingElapsedSeconds = null; // Preserve final elapsed time
|
|
167
|
+
aiRuntimeStart = null;
|
|
168
|
+
aiRuntimeTotalMs = 0;
|
|
166
169
|
streamingFormatter = null;
|
|
167
170
|
statusLineState = null;
|
|
168
171
|
statusMessageOverride = null;
|
|
169
172
|
promptRefreshTimer = null;
|
|
170
173
|
launchPaletteShown = false;
|
|
174
|
+
launchBannerText = null;
|
|
171
175
|
version;
|
|
172
176
|
alternateScreenEnabled;
|
|
173
177
|
constructor(config) {
|
|
@@ -373,7 +377,30 @@ export class InteractiveShell {
|
|
|
373
377
|
display.showInfo(this.sessionResumeNotice);
|
|
374
378
|
this.sessionResumeNotice = null;
|
|
375
379
|
}
|
|
380
|
+
renderWelcomeBanner(force = false) {
|
|
381
|
+
if (isPlainOutputMode() || !output.isTTY) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const header = `${theme.fields.model(this.sessionState.model)} ${theme.ui.muted('@')} ${theme.fields.agent(this.providerLabel(this.sessionState.provider))}`;
|
|
385
|
+
const profileLine = `${theme.fields.profile(this.profileLabel)} ${theme.ui.muted(`(${this.profile})`)}`;
|
|
386
|
+
const workspaceLine = theme.fields.workspace(this.workingDir);
|
|
387
|
+
const versionLine = this.version ? theme.ui.muted(`v${this.version} · support@ero.solar`) : null;
|
|
388
|
+
const hintLine = theme.ui.muted('/help for commands · /model to switch · /secrets for keys');
|
|
389
|
+
const width = output.columns ?? 80;
|
|
390
|
+
const banner = [header, profileLine, workspaceLine, versionLine, hintLine, renderDivider(width)]
|
|
391
|
+
.filter(Boolean)
|
|
392
|
+
.join('\n');
|
|
393
|
+
if (!force && this.launchBannerText) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (this.launchBannerText === banner) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
display.stream(`${banner}\n`);
|
|
400
|
+
this.launchBannerText = banner;
|
|
401
|
+
}
|
|
376
402
|
async start(initialPrompt) {
|
|
403
|
+
this.renderWelcomeBanner();
|
|
377
404
|
if (initialPrompt) {
|
|
378
405
|
await this.processInputBlock(initialPrompt);
|
|
379
406
|
return;
|
|
@@ -583,26 +610,35 @@ export class InteractiveShell {
|
|
|
583
610
|
}
|
|
584
611
|
// Trigger context compaction
|
|
585
612
|
display.showInfo('Compacting context... This will summarize the conversation and free up space.');
|
|
586
|
-
void this.performContextCompaction();
|
|
613
|
+
void this.performContextCompaction('Manual shortcut compaction');
|
|
587
614
|
}
|
|
588
615
|
/**
|
|
589
616
|
* Perform context compaction by summarizing conversation history.
|
|
590
617
|
*/
|
|
591
|
-
async performContextCompaction() {
|
|
618
|
+
async performContextCompaction(reason = 'Manual context compaction') {
|
|
592
619
|
try {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (oldLength === 0) {
|
|
597
|
-
display.showInfo('Context is already empty.');
|
|
620
|
+
const agent = this.agent;
|
|
621
|
+
if (!agent) {
|
|
622
|
+
display.showWarning('No active agent to compact context.');
|
|
598
623
|
return;
|
|
599
624
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
625
|
+
const contextManager = agent.getContextManager();
|
|
626
|
+
if (!contextManager) {
|
|
627
|
+
display.showWarning('Context manager unavailable. Try sending a message first.');
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const history = agent.getHistory();
|
|
631
|
+
if (history.length <= 1) {
|
|
632
|
+
display.showInfo('Context is already minimal.');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const stats = contextManager.getStats(history);
|
|
636
|
+
const windowTokens = this.activeContextWindowTokens ?? this.latestTokenUsage.limit ?? null;
|
|
637
|
+
await this.runContextCleanup(windowTokens, stats.totalTokens, {
|
|
638
|
+
reason,
|
|
639
|
+
forced: true,
|
|
640
|
+
source: 'manual',
|
|
641
|
+
});
|
|
606
642
|
}
|
|
607
643
|
catch (error) {
|
|
608
644
|
display.showError('Failed to compact context', error);
|
|
@@ -714,6 +750,7 @@ export class InteractiveShell {
|
|
|
714
750
|
// Stop any active spinner to prevent process hang
|
|
715
751
|
display.stopThinking(false);
|
|
716
752
|
this.stopStreamingHeartbeat();
|
|
753
|
+
this.endAiRuntime();
|
|
717
754
|
this.uiUpdates.dispose();
|
|
718
755
|
this.clearPromptRefreshTimer();
|
|
719
756
|
this.teardownStatusTracking();
|
|
@@ -1159,6 +1196,25 @@ export class InteractiveShell {
|
|
|
1159
1196
|
process.stdout.write(content);
|
|
1160
1197
|
}, 'interactiveShell.stdout');
|
|
1161
1198
|
}
|
|
1199
|
+
beginAiRuntime() {
|
|
1200
|
+
if (this.aiRuntimeStart === null) {
|
|
1201
|
+
this.aiRuntimeStart = Date.now();
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
endAiRuntime() {
|
|
1205
|
+
if (this.aiRuntimeStart !== null) {
|
|
1206
|
+
this.aiRuntimeTotalMs += Date.now() - this.aiRuntimeStart;
|
|
1207
|
+
this.aiRuntimeStart = null;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
getAiRuntimeSeconds() {
|
|
1211
|
+
const runningMs = this.aiRuntimeStart ? Date.now() - this.aiRuntimeStart : 0;
|
|
1212
|
+
const totalMs = this.aiRuntimeTotalMs + runningMs;
|
|
1213
|
+
if (totalMs <= 0 && this.aiRuntimeStart === null) {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
return Math.max(0, Math.floor(totalMs / 1000));
|
|
1217
|
+
}
|
|
1162
1218
|
/**
|
|
1163
1219
|
* Refresh the status line in the persistent input area.
|
|
1164
1220
|
* Uses combined status display: streaming label + override + main status.
|
|
@@ -1173,20 +1229,7 @@ export class InteractiveShell {
|
|
|
1173
1229
|
const statusText = this.formatStatusLine(this.statusLineState);
|
|
1174
1230
|
this.terminalInput.setStatusMessage(statusText);
|
|
1175
1231
|
// Surface meta header (elapsed + context usage) above the divider
|
|
1176
|
-
|
|
1177
|
-
let elapsedSeconds = null;
|
|
1178
|
-
if (this.streamingHeartbeatStart) {
|
|
1179
|
-
// Actively streaming - compute live elapsed
|
|
1180
|
-
elapsedSeconds = Math.max(0, Math.floor((Date.now() - this.streamingHeartbeatStart) / 1000));
|
|
1181
|
-
}
|
|
1182
|
-
else if (this.lastStreamingElapsedSeconds !== null) {
|
|
1183
|
-
// Just finished streaming - use preserved final time
|
|
1184
|
-
elapsedSeconds = this.lastStreamingElapsedSeconds;
|
|
1185
|
-
}
|
|
1186
|
-
else if (this.statusLineState) {
|
|
1187
|
-
// Fallback to status line state elapsed
|
|
1188
|
-
elapsedSeconds = Math.max(0, Math.floor((Date.now() - this.statusLineState.startedAt) / 1000));
|
|
1189
|
-
}
|
|
1232
|
+
const elapsedSeconds = this.getAiRuntimeSeconds();
|
|
1190
1233
|
const thinkingMs = display.isSpinnerActive() ? display.getThinkingElapsedMs() : null;
|
|
1191
1234
|
const tokensUsed = this.latestTokenUsage.used;
|
|
1192
1235
|
const tokenLimit = this.latestTokenUsage.limit ?? this.activeContextWindowTokens;
|
|
@@ -1365,6 +1408,8 @@ export class InteractiveShell {
|
|
|
1365
1408
|
this.terminalInput.streamContent(closing);
|
|
1366
1409
|
}
|
|
1367
1410
|
this.streamingFormatter = null;
|
|
1411
|
+
// Force the prompt to re-render after streaming so it stays below the last response
|
|
1412
|
+
this.requestPromptRefresh(true);
|
|
1368
1413
|
}
|
|
1369
1414
|
buildStreamingStatus(label, _elapsedSeconds) {
|
|
1370
1415
|
// Model + elapsed time already live in the pinned meta header; keep the streaming
|
|
@@ -1678,7 +1723,7 @@ export class InteractiveShell {
|
|
|
1678
1723
|
this.handleCostCommand();
|
|
1679
1724
|
break;
|
|
1680
1725
|
case '/usage':
|
|
1681
|
-
this.handleUsageCommand();
|
|
1726
|
+
this.handleUsageCommand(input);
|
|
1682
1727
|
break;
|
|
1683
1728
|
case '/clear':
|
|
1684
1729
|
this.handleClearCommand();
|
|
@@ -3465,7 +3510,13 @@ export class InteractiveShell {
|
|
|
3465
3510
|
lines.push(theme.ui.muted('Token usage is tracked automatically during the session.'));
|
|
3466
3511
|
display.showSystemMessage(lines.join('\n'));
|
|
3467
3512
|
}
|
|
3468
|
-
handleUsageCommand() {
|
|
3513
|
+
handleUsageCommand(input) {
|
|
3514
|
+
const tokens = input ? input.trim().split(/\s+/) : [];
|
|
3515
|
+
const subcommand = tokens[1]?.toLowerCase();
|
|
3516
|
+
if (subcommand === 'history' || subcommand === 'log') {
|
|
3517
|
+
this.showCompactionHistory();
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3469
3520
|
const percentage = this.latestTokenUsage.limit && this.latestTokenUsage.used
|
|
3470
3521
|
? Math.round((this.latestTokenUsage.used / this.latestTokenUsage.limit) * 100)
|
|
3471
3522
|
: 0;
|
|
@@ -3486,6 +3537,37 @@ export class InteractiveShell {
|
|
|
3486
3537
|
else {
|
|
3487
3538
|
lines.push(theme.success('Plenty of context available.'));
|
|
3488
3539
|
}
|
|
3540
|
+
if (this.contextCompactionLog.length > 0) {
|
|
3541
|
+
lines.push('');
|
|
3542
|
+
lines.push(theme.secondary('Tip: /usage history shows recent context compactions.'));
|
|
3543
|
+
}
|
|
3544
|
+
display.showSystemMessage(lines.join('\n'));
|
|
3545
|
+
}
|
|
3546
|
+
showCompactionHistory() {
|
|
3547
|
+
if (!this.contextCompactionLog.length) {
|
|
3548
|
+
display.showInfo('No context compactions recorded yet.');
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
3551
|
+
const entries = [...this.contextCompactionLog].slice(-10).reverse();
|
|
3552
|
+
const lines = [];
|
|
3553
|
+
lines.push(theme.bold('Context Compaction History'));
|
|
3554
|
+
lines.push('');
|
|
3555
|
+
for (const entry of entries) {
|
|
3556
|
+
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
3557
|
+
const reason = entry.reason || (entry.source === 'auto' ? 'Auto-compaction' : 'Manual compaction');
|
|
3558
|
+
const before = entry.beforeTokens.toLocaleString('en-US');
|
|
3559
|
+
const after = entry.afterTokens.toLocaleString('en-US');
|
|
3560
|
+
const saved = Math.max(0, entry.beforeTokens - entry.afterTokens);
|
|
3561
|
+
const savedLabel = saved > 0 ? `${saved.toLocaleString('en-US')} saved` : 'no savings';
|
|
3562
|
+
const pctBefore = entry.percentBefore ?? null;
|
|
3563
|
+
const pctAfter = entry.percentAfter ?? null;
|
|
3564
|
+
const pctLabel = pctBefore !== null && pctAfter !== null
|
|
3565
|
+
? `(${pctBefore}% → ${pctAfter}%)`
|
|
3566
|
+
: '';
|
|
3567
|
+
const summaryNote = entry.summarized ? 'summary applied' : 'prune only';
|
|
3568
|
+
lines.push(` ${time} · ${reason} [${entry.source} · ${entry.model}]`);
|
|
3569
|
+
lines.push(` ${before} → ${after} tokens ${pctLabel} · ${savedLabel} · ${summaryNote}`);
|
|
3570
|
+
}
|
|
3489
3571
|
display.showSystemMessage(lines.join('\n'));
|
|
3490
3572
|
}
|
|
3491
3573
|
handleClearCommand() {
|
|
@@ -3622,13 +3704,7 @@ export class InteractiveShell {
|
|
|
3622
3704
|
return;
|
|
3623
3705
|
}
|
|
3624
3706
|
display.showInfo('Compacting conversation context...');
|
|
3625
|
-
|
|
3626
|
-
const { used, limit } = this.latestTokenUsage;
|
|
3627
|
-
if (used && limit && used / limit < 0.5) {
|
|
3628
|
-
display.showInfo('Context usage is low. No compaction needed.');
|
|
3629
|
-
return;
|
|
3630
|
-
}
|
|
3631
|
-
await this.processRequest('Please summarize our conversation so far in a concise way, preserving key decisions, code changes, and next steps. This will help compact the context.');
|
|
3707
|
+
await this.performContextCompaction('Manual /compact request');
|
|
3632
3708
|
}
|
|
3633
3709
|
// ==================== End Claude Code Style Commands ====================
|
|
3634
3710
|
updateActiveSession(summary, remember = false) {
|
|
@@ -4570,6 +4646,7 @@ export class InteractiveShell {
|
|
|
4570
4646
|
this.currentToolCalls = [];
|
|
4571
4647
|
this.uiAdapter.startProcessing('Working on your request');
|
|
4572
4648
|
this.setProcessingStatus();
|
|
4649
|
+
this.beginAiRuntime();
|
|
4573
4650
|
let responseText = '';
|
|
4574
4651
|
try {
|
|
4575
4652
|
// Start streaming - no header needed, the input area already provides context
|
|
@@ -4635,6 +4712,7 @@ export class InteractiveShell {
|
|
|
4635
4712
|
display.stopThinking(false);
|
|
4636
4713
|
this.uiUpdates.setMode('processing');
|
|
4637
4714
|
this.stopStreamingHeartbeat();
|
|
4715
|
+
this.endAiRuntime();
|
|
4638
4716
|
this.isProcessing = false;
|
|
4639
4717
|
this.terminalInput.setStreaming(false);
|
|
4640
4718
|
this.uiAdapter.endProcessing('Ready for prompts');
|
|
@@ -4690,6 +4768,7 @@ export class InteractiveShell {
|
|
|
4690
4768
|
display.showSystemMessage(`Continuous mode active. Ctrl+C to stop.`);
|
|
4691
4769
|
this.uiAdapter.startProcessing('Continuous execution mode');
|
|
4692
4770
|
this.setProcessingStatus();
|
|
4771
|
+
this.beginAiRuntime();
|
|
4693
4772
|
// No streaming header - just start streaming directly
|
|
4694
4773
|
this.startStreamingHeartbeat('Streaming');
|
|
4695
4774
|
let iteration = 0;
|
|
@@ -4865,6 +4944,7 @@ What's the next action?`;
|
|
|
4865
4944
|
resetTaskCompletionDetector();
|
|
4866
4945
|
this.uiUpdates.setMode('processing');
|
|
4867
4946
|
this.stopStreamingHeartbeat();
|
|
4947
|
+
this.endAiRuntime();
|
|
4868
4948
|
this.isProcessing = false;
|
|
4869
4949
|
this.terminalInput.setStreaming(false);
|
|
4870
4950
|
this.uiAdapter.endProcessing('Ready for prompts');
|
|
@@ -5421,6 +5501,7 @@ Return ONLY JSON array:
|
|
|
5421
5501
|
try {
|
|
5422
5502
|
ensureSecretForProvider(this.sessionState.provider);
|
|
5423
5503
|
this.runtimeSession.updateToolContext(this.sessionState);
|
|
5504
|
+
this.ensureActiveSummarizer(this.runtimeSession.contextManager);
|
|
5424
5505
|
const selection = {
|
|
5425
5506
|
provider: this.sessionState.provider,
|
|
5426
5507
|
model: this.sessionState.model,
|
|
@@ -5571,6 +5652,7 @@ Return ONLY JSON array:
|
|
|
5571
5652
|
* Ensures the input area is visible and ready for input, just like on startup.
|
|
5572
5653
|
*/
|
|
5573
5654
|
resetChatBoxAfterModelSwap() {
|
|
5655
|
+
this.renderWelcomeBanner(true);
|
|
5574
5656
|
this.updateStatusMessage(null);
|
|
5575
5657
|
this.terminalInput.setStreaming(false);
|
|
5576
5658
|
this.terminalInput.render();
|
|
@@ -5686,7 +5768,7 @@ Return ONLY JSON array:
|
|
|
5686
5768
|
if (!trigger) {
|
|
5687
5769
|
return null;
|
|
5688
5770
|
}
|
|
5689
|
-
return this.runContextCleanup(windowTokens, total, trigger);
|
|
5771
|
+
return this.runContextCleanup(windowTokens, total, { ...trigger, source: 'auto' });
|
|
5690
5772
|
}
|
|
5691
5773
|
totalTokens(usage) {
|
|
5692
5774
|
if (!usage) {
|
|
@@ -5713,58 +5795,104 @@ Return ONLY JSON array:
|
|
|
5713
5795
|
if (history.length <= 1) {
|
|
5714
5796
|
return;
|
|
5715
5797
|
}
|
|
5798
|
+
if (!this.areToolTurnsComplete(history)) {
|
|
5799
|
+
display.showInfo('Context compaction postponed until all tool calls have returned results.');
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
this.ensureActiveSummarizer(contextManager);
|
|
5716
5803
|
this.cleanupInProgress = true;
|
|
5717
5804
|
this.contextCompactionInFlight = true;
|
|
5718
5805
|
const cleanupStatusId = 'context-cleanup';
|
|
5719
5806
|
let cleanupOverlayActive = false;
|
|
5720
5807
|
try {
|
|
5721
|
-
const
|
|
5722
|
-
const
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5808
|
+
const beforeStats = contextManager.getStats(history);
|
|
5809
|
+
const resolvedWindowTokens = windowTokens ?? this.activeContextWindowTokens ?? this.latestTokenUsage.limit ?? null;
|
|
5810
|
+
const resolvedTotalTokens = totalTokens ?? beforeStats.totalTokens;
|
|
5811
|
+
const percentUsed = resolvedWindowTokens && resolvedWindowTokens > 0
|
|
5812
|
+
? Math.min(100, Math.round((resolvedTotalTokens / resolvedWindowTokens) * 100))
|
|
5813
|
+
: beforeStats.percentage;
|
|
5814
|
+
const statusDetailParts = [
|
|
5815
|
+
`${percentUsed}% full`,
|
|
5816
|
+
trigger?.reason,
|
|
5817
|
+
].filter(Boolean);
|
|
5726
5818
|
this.statusTracker.pushOverride(cleanupStatusId, 'Running context cleanup', {
|
|
5727
5819
|
detail: statusDetailParts.join(' · '),
|
|
5728
5820
|
tone: 'warning',
|
|
5729
5821
|
});
|
|
5730
5822
|
cleanupOverlayActive = true;
|
|
5731
5823
|
const triggerReason = trigger?.reason ?? 'Context optimization';
|
|
5824
|
+
const limitLabel = resolvedWindowTokens
|
|
5825
|
+
? `of ${resolvedWindowTokens.toLocaleString('en-US')} tokens`
|
|
5826
|
+
: 'of estimated context';
|
|
5732
5827
|
display.showSystemMessage([
|
|
5733
|
-
`Context usage: ${
|
|
5828
|
+
`Context usage: ${resolvedTotalTokens.toLocaleString('en-US')} ${limitLabel}`,
|
|
5734
5829
|
`(${percentUsed}% full). Auto-compacting${triggerReason ? `: ${triggerReason}` : '...'}`,
|
|
5735
5830
|
].join(' '));
|
|
5736
|
-
const beforeStats = contextManager.getStats(history);
|
|
5737
5831
|
const result = await contextManager.intelligentCompact(history);
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
const
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5832
|
+
let afterStats = contextManager.getStats(result.compacted);
|
|
5833
|
+
let appliedHistory = result.compacted;
|
|
5834
|
+
let appliedSummarized = result.summarized;
|
|
5835
|
+
const initialTokenSavings = Math.max(0, beforeStats.totalTokens - afterStats.totalTokens);
|
|
5836
|
+
let bestTokenSavings = initialTokenSavings;
|
|
5837
|
+
let bestPercentSavings = beforeStats.totalTokens > 0 ? (initialTokenSavings / beforeStats.totalTokens) * 100 : 0;
|
|
5838
|
+
let changed = this.historiesDiffer(history, appliedHistory);
|
|
5839
|
+
if (!this.hasMeaningfulCompaction(changed, bestTokenSavings, bestPercentSavings, trigger?.forced)) {
|
|
5840
|
+
const fallback = await contextManager.pruneMessagesWithSummary(history, { force: true });
|
|
5841
|
+
const fallbackStats = contextManager.getStats(fallback.pruned);
|
|
5842
|
+
const fallbackTokenSavings = Math.max(0, beforeStats.totalTokens - fallbackStats.totalTokens);
|
|
5843
|
+
const fallbackPercentSavings = beforeStats.totalTokens > 0
|
|
5844
|
+
? (fallbackTokenSavings / beforeStats.totalTokens) * 100
|
|
5845
|
+
: 0;
|
|
5846
|
+
if (this.historiesDiffer(history, fallback.pruned) &&
|
|
5847
|
+
(fallbackTokenSavings > bestTokenSavings || !changed)) {
|
|
5848
|
+
appliedHistory = fallback.pruned;
|
|
5849
|
+
afterStats = fallbackStats;
|
|
5850
|
+
appliedSummarized = fallback.summarized;
|
|
5851
|
+
bestTokenSavings = fallbackTokenSavings;
|
|
5852
|
+
bestPercentSavings = fallbackPercentSavings;
|
|
5853
|
+
changed = true;
|
|
5747
5854
|
}
|
|
5855
|
+
}
|
|
5856
|
+
if (!changed) {
|
|
5857
|
+
display.showInfo('Context compaction completed but no changes were applied.');
|
|
5858
|
+
return;
|
|
5859
|
+
}
|
|
5860
|
+
if (!this.hasMeaningfulCompaction(changed, bestTokenSavings, bestPercentSavings, trigger?.forced)) {
|
|
5861
|
+
display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
|
|
5748
5862
|
return;
|
|
5749
5863
|
}
|
|
5750
|
-
agent.loadHistory(
|
|
5751
|
-
this.cachedHistory =
|
|
5752
|
-
const newPercentUsed =
|
|
5753
|
-
? Math.round((afterStats.totalTokens /
|
|
5864
|
+
agent.loadHistory(appliedHistory);
|
|
5865
|
+
this.cachedHistory = appliedHistory;
|
|
5866
|
+
const newPercentUsed = resolvedWindowTokens && resolvedWindowTokens > 0
|
|
5867
|
+
? Math.min(100, Math.round((afterStats.totalTokens / resolvedWindowTokens) * 100))
|
|
5754
5868
|
: afterStats.percentage;
|
|
5755
5869
|
this.latestTokenUsage = {
|
|
5756
5870
|
used: afterStats.totalTokens,
|
|
5757
|
-
limit:
|
|
5871
|
+
limit: resolvedWindowTokens,
|
|
5758
5872
|
};
|
|
5759
5873
|
this.updateContextUsage(newPercentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
|
|
5760
5874
|
this.lastContextWarningLevel = this.getContextWarningLevel(newPercentUsed);
|
|
5761
5875
|
this.refreshStatusLine(true);
|
|
5762
5876
|
const primarySignal = result.analysis.signals[0]?.reason ?? triggerReason;
|
|
5877
|
+
const savingsLabel = bestTokenSavings > 0
|
|
5878
|
+
? `(saved ~${bestTokenSavings.toLocaleString('en-US')} tokens, ~${bestPercentSavings.toFixed(1)}%)`
|
|
5879
|
+
: '(no token savings)';
|
|
5763
5880
|
display.showSystemMessage([
|
|
5764
5881
|
`Context compacted: ${beforeStats.percentage}% → ${afterStats.percentage}%`,
|
|
5765
|
-
|
|
5882
|
+
savingsLabel,
|
|
5766
5883
|
primarySignal ? `Reason: ${primarySignal}.` : '',
|
|
5767
5884
|
].filter(Boolean).join(' '));
|
|
5885
|
+
this.recordContextCompaction({
|
|
5886
|
+
timestamp: Date.now(),
|
|
5887
|
+
source: trigger?.source ?? 'auto',
|
|
5888
|
+
reason: triggerReason,
|
|
5889
|
+
beforeTokens: beforeStats.totalTokens,
|
|
5890
|
+
afterTokens: afterStats.totalTokens,
|
|
5891
|
+
summarized: appliedSummarized,
|
|
5892
|
+
percentBefore: resolvedWindowTokens ? percentUsed : beforeStats.percentage,
|
|
5893
|
+
percentAfter: newPercentUsed,
|
|
5894
|
+
model: this.sessionState.model,
|
|
5895
|
+
});
|
|
5768
5896
|
}
|
|
5769
5897
|
catch (error) {
|
|
5770
5898
|
display.showError('Context compaction failed.', error);
|
|
@@ -5777,7 +5905,35 @@ Return ONLY JSON array:
|
|
|
5777
5905
|
this.contextCompactionInFlight = false;
|
|
5778
5906
|
}
|
|
5779
5907
|
}
|
|
5780
|
-
|
|
5908
|
+
recordContextCompaction(event) {
|
|
5909
|
+
this.contextCompactionLog.push(event);
|
|
5910
|
+
if (this.contextCompactionLog.length > 20) {
|
|
5911
|
+
this.contextCompactionLog.shift();
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
ensureActiveSummarizer(contextManager) {
|
|
5915
|
+
if (!contextManager?.updateConfig) {
|
|
5916
|
+
return;
|
|
5917
|
+
}
|
|
5918
|
+
const summarizer = async (messages) => {
|
|
5919
|
+
const summary = await this.buildContextSummary(messages);
|
|
5920
|
+
return summary ?? '[No content to summarize]';
|
|
5921
|
+
};
|
|
5922
|
+
contextManager.updateConfig({
|
|
5923
|
+
summarizationCallback: summarizer,
|
|
5924
|
+
useLLMSummarization: true,
|
|
5925
|
+
});
|
|
5926
|
+
}
|
|
5927
|
+
hasMeaningfulCompaction(changed, tokenSavings, percentSavings, forced) {
|
|
5928
|
+
if (!changed) {
|
|
5929
|
+
return false;
|
|
5930
|
+
}
|
|
5931
|
+
if (tokenSavings >= MIN_COMPACTION_TOKEN_SAVINGS || percentSavings >= MIN_COMPACTION_PERCENT_SAVINGS) {
|
|
5932
|
+
return true;
|
|
5933
|
+
}
|
|
5934
|
+
return Boolean(forced && tokenSavings > 0);
|
|
5935
|
+
}
|
|
5936
|
+
shouldAutoCompactContext(usageRatio, _windowTokens, _totalTokens) {
|
|
5781
5937
|
const featureFlags = loadFeatureFlags();
|
|
5782
5938
|
if (featureFlags.autoCompact === false) {
|
|
5783
5939
|
return null;
|
|
@@ -5898,6 +6054,25 @@ Return ONLY JSON array:
|
|
|
5898
6054
|
}
|
|
5899
6055
|
return false;
|
|
5900
6056
|
}
|
|
6057
|
+
areToolTurnsComplete(messages) {
|
|
6058
|
+
const pending = new Set();
|
|
6059
|
+
for (const message of messages) {
|
|
6060
|
+
if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
|
|
6061
|
+
for (const call of message.toolCalls) {
|
|
6062
|
+
if (call.id) {
|
|
6063
|
+
pending.add(call.id);
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
if (message.role === 'tool') {
|
|
6068
|
+
const toolCallId = message.toolCallId;
|
|
6069
|
+
if (toolCallId && pending.has(toolCallId)) {
|
|
6070
|
+
pending.delete(toolCallId);
|
|
6071
|
+
}
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
return pending.size === 0;
|
|
6075
|
+
}
|
|
5901
6076
|
partitionHistory(history) {
|
|
5902
6077
|
const system = [];
|
|
5903
6078
|
const conversation = [];
|
|
@@ -5989,10 +6164,10 @@ Return ONLY JSON array:
|
|
|
5989
6164
|
sections.push(`Conversation chunk:\n${chunk}`);
|
|
5990
6165
|
sections.push([
|
|
5991
6166
|
'Instructions:',
|
|
5992
|
-
'- Merge
|
|
5993
|
-
'-
|
|
5994
|
-
'-
|
|
5995
|
-
'-
|
|
6167
|
+
'- Merge with the running summary.',
|
|
6168
|
+
'- Keep TODOs/bugs/test gaps and file references.',
|
|
6169
|
+
'- Mark what is done vs. pending.',
|
|
6170
|
+
'- Stay concise (<180 words) with short bullets.',
|
|
5996
6171
|
].join('\n'));
|
|
5997
6172
|
return sections.join('\n\n');
|
|
5998
6173
|
}
|