@nghyane/arcane 0.1.28 → 0.1.30
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/CHANGELOG.md +7 -0
- package/package.json +4 -4
- package/src/cli/config-cli.ts +1 -1
- package/src/config/settings-schema.ts +19 -27
- package/src/config/settings.ts +3 -4
- package/src/extensibility/custom-tools/types.ts +0 -12
- package/src/extensibility/extensions/index.ts +0 -5
- package/src/extensibility/extensions/runner.ts +6 -26
- package/src/extensibility/extensions/types.ts +1 -77
- package/src/extensibility/hooks/runner.ts +5 -24
- package/src/extensibility/hooks/types.ts +1 -77
- package/src/index.ts +2 -13
- package/src/modes/components/footer.ts +4 -11
- package/src/modes/components/index.ts +0 -1
- package/src/modes/components/status-line/segments.ts +1 -2
- package/src/modes/components/status-line/types.ts +0 -1
- package/src/modes/components/status-line.ts +0 -6
- package/src/modes/components/tree-selector.ts +0 -8
- package/src/modes/controllers/command-controller.ts +2 -98
- package/src/modes/controllers/event-controller.ts +46 -52
- package/src/modes/controllers/extension-ui-controller.ts +0 -42
- package/src/modes/controllers/input-controller.ts +0 -23
- package/src/modes/controllers/selector-controller.ts +0 -5
- package/src/modes/interactive-mode.ts +3 -24
- package/src/modes/print-mode.ts +0 -16
- package/src/modes/rpc/rpc-client.ts +0 -16
- package/src/modes/rpc/rpc-mode.ts +0 -32
- package/src/modes/rpc/rpc-types.ts +0 -9
- package/src/modes/types.ts +1 -13
- package/src/modes/utils/ui-helpers.ts +2 -118
- package/src/prompts/agents/librarian.md +7 -12
- package/src/sdk.ts +0 -15
- package/src/session/agent-session.ts +89 -650
- package/src/session/compaction/branch-summarization.ts +5 -13
- package/src/session/compaction/index.ts +0 -1
- package/src/session/compaction/utils.ts +94 -2
- package/src/session/messages.ts +0 -37
- package/src/session/retry-utils.ts +1 -1
- package/src/session/session-manager.ts +8 -108
- package/src/session/session-types.ts +4 -25
- package/src/session/stats.ts +2 -39
- package/src/slash-commands/builtin-registry.ts +0 -11
- package/src/task/executor.ts +0 -8
- package/src/tools/create-tools.ts +3 -0
- package/src/tools/github-fs.ts +195 -0
- package/src/tools/github-utils.ts +35 -0
- package/src/tools/github.ts +35 -123
- package/src/tools/index.ts +1 -0
- package/examples/hooks/custom-compaction.ts +0 -116
- package/src/modes/components/compaction-summary-message.ts +0 -59
- package/src/prompts/compaction/compaction-short-summary.md +0 -9
- package/src/prompts/compaction/compaction-summary-context.md +0 -5
- package/src/prompts/compaction/compaction-summary.md +0 -41
- package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
- package/src/prompts/compaction/compaction-update-summary.md +0 -45
- package/src/session/compaction/compaction.ts +0 -864
- package/src/session/compaction/pruning.ts +0 -91
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* - Agent state access
|
|
7
7
|
* - Event subscription with automatic session persistence
|
|
8
8
|
* - Model and thinking level management
|
|
9
|
-
* - Compaction (manual and auto)
|
|
10
9
|
* - Bash execution
|
|
11
10
|
* - Session switching and branching
|
|
12
11
|
*
|
|
@@ -34,7 +33,7 @@ import type {
|
|
|
34
33
|
ToolChoice,
|
|
35
34
|
UsageReport,
|
|
36
35
|
} from "@nghyane/arcane-ai";
|
|
37
|
-
import { isContextOverflow
|
|
36
|
+
import { isContextOverflow } from "@nghyane/arcane-ai";
|
|
38
37
|
import { abortableSleep, isEnoent, logger } from "@nghyane/arcane-utils";
|
|
39
38
|
import { getAgentDbPath } from "@nghyane/arcane-utils/dirs";
|
|
40
39
|
import type { ModelRegistry, ModelRole } from "../config/model-registry";
|
|
@@ -58,7 +57,6 @@ import type {
|
|
|
58
57
|
MessageStartEvent,
|
|
59
58
|
MessageUpdateEvent,
|
|
60
59
|
SessionBeforeBranchResult,
|
|
61
|
-
SessionBeforeCompactResult,
|
|
62
60
|
SessionBeforeSwitchResult,
|
|
63
61
|
SessionBeforeTreeResult,
|
|
64
62
|
ToolExecutionEndEvent,
|
|
@@ -68,7 +66,7 @@ import type {
|
|
|
68
66
|
TurnEndEvent,
|
|
69
67
|
TurnStartEvent,
|
|
70
68
|
} from "../extensibility/extensions";
|
|
71
|
-
import type {
|
|
69
|
+
import type { ContextUsage } from "../extensibility/extensions/types";
|
|
72
70
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
73
71
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
74
72
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
@@ -83,21 +81,11 @@ import { outputMeta } from "../tools/output-meta";
|
|
|
83
81
|
import type { TodoItem } from "../tools/todo-write";
|
|
84
82
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
85
83
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
86
|
-
import {
|
|
87
|
-
type CompactionResult,
|
|
88
|
-
calculateContextTokens,
|
|
89
|
-
collectEntriesForBranchSummary,
|
|
90
|
-
compact,
|
|
91
|
-
generateBranchSummary,
|
|
92
|
-
prepareCompaction,
|
|
93
|
-
shouldCompact,
|
|
94
|
-
} from "./compaction";
|
|
95
|
-
import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "./compaction/pruning";
|
|
84
|
+
import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction";
|
|
96
85
|
import type { BashExecutionMessage, CustomMessage, PythonExecutionMessage } from "./messages";
|
|
97
86
|
import { ModelController } from "./model-controller";
|
|
98
87
|
import { isRetryableErrorMessage, isUsageLimitErrorMessage, parseRetryAfterMs } from "./retry-utils";
|
|
99
|
-
import type { BranchSummaryEntry,
|
|
100
|
-
import { getLatestCompactionEntry } from "./session-manager";
|
|
88
|
+
import type { BranchSummaryEntry, NewSessionOptions, SessionManager } from "./session-manager";
|
|
101
89
|
import type {
|
|
102
90
|
AgentSessionConfig,
|
|
103
91
|
AgentSessionEvent,
|
|
@@ -208,10 +196,6 @@ export class AgentSession {
|
|
|
208
196
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
209
197
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
210
198
|
|
|
211
|
-
// Compaction state
|
|
212
|
-
#compactionAbortController: AbortController | undefined = undefined;
|
|
213
|
-
#autoCompactionAbortController: AbortController | undefined = undefined;
|
|
214
|
-
|
|
215
199
|
// Branch summarization state
|
|
216
200
|
#branchSummaryAbortController: AbortController | undefined = undefined;
|
|
217
201
|
|
|
@@ -228,6 +212,8 @@ export class AgentSession {
|
|
|
228
212
|
// Todo completion reminder state
|
|
229
213
|
#todoReminderCount = 0;
|
|
230
214
|
|
|
215
|
+
// Auto-handoff state
|
|
216
|
+
#contextWarningEmitted = false;
|
|
231
217
|
// Verification loop state
|
|
232
218
|
#verificationReminderCount = 0;
|
|
233
219
|
#turnHasFileModifications = false;
|
|
@@ -290,7 +276,7 @@ export class AgentSession {
|
|
|
290
276
|
this.agent.providerSessionState = this.#model.providerSessionState;
|
|
291
277
|
|
|
292
278
|
// Always subscribe to agent events for internal handling
|
|
293
|
-
// (session persistence, hooks,
|
|
279
|
+
// (session persistence, hooks, retry logic)
|
|
294
280
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
295
281
|
}
|
|
296
282
|
|
|
@@ -332,9 +318,6 @@ export class AgentSession {
|
|
|
332
318
|
this.#emit(event);
|
|
333
319
|
}
|
|
334
320
|
|
|
335
|
-
// Track last assistant message for auto-compaction check
|
|
336
|
-
#lastAssistantMessage: AssistantMessage | undefined = undefined;
|
|
337
|
-
|
|
338
321
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
339
322
|
#handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
340
323
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
@@ -497,11 +480,9 @@ export class AgentSession {
|
|
|
497
480
|
// Regular LLM message - persist as SessionMessageEntry
|
|
498
481
|
this.sessionManager.appendMessage(event.message);
|
|
499
482
|
}
|
|
500
|
-
// Other message types (bashExecution,
|
|
483
|
+
// Other message types (bashExecution, branchSummary) are persisted elsewhere
|
|
501
484
|
|
|
502
|
-
// Track assistant message for auto-compaction (checked on agent_end)
|
|
503
485
|
if (event.message.role === "assistant") {
|
|
504
|
-
this.#lastAssistantMessage = event.message;
|
|
505
486
|
const assistantMsg = event.message as AssistantMessage;
|
|
506
487
|
queueDeferredTtsrInjectionIfNeeded(this.#ttsr, this.agent, assistantMsg);
|
|
507
488
|
if (this.#handoffAbortController) {
|
|
@@ -578,24 +559,26 @@ export class AgentSession {
|
|
|
578
559
|
}
|
|
579
560
|
}
|
|
580
561
|
|
|
581
|
-
// Check auto-retry
|
|
582
|
-
if (event.type === "agent_end"
|
|
583
|
-
const msg = this.#
|
|
584
|
-
|
|
562
|
+
// Check auto-retry after agent completes
|
|
563
|
+
if (event.type === "agent_end") {
|
|
564
|
+
const msg = this.#findLastAssistantMessage();
|
|
565
|
+
if (!msg) return;
|
|
585
566
|
|
|
586
567
|
if (this.#skipPostTurnMaintenanceAssistantTimestamp === msg.timestamp) {
|
|
587
568
|
this.#skipPostTurnMaintenanceAssistantTimestamp = undefined;
|
|
588
569
|
return;
|
|
589
570
|
}
|
|
590
571
|
|
|
572
|
+
// Check for auto-handoff (context overflow or high usage)
|
|
573
|
+
const didHandoff = await this.#checkAutoHandoff(msg);
|
|
574
|
+
if (didHandoff) return;
|
|
575
|
+
|
|
591
576
|
// Check for retryable errors first (overloaded, rate limit, server errors)
|
|
592
577
|
if (this.#isRetryableError(msg)) {
|
|
593
578
|
const didRetry = await this.#handleRetryableError(msg);
|
|
594
|
-
if (didRetry) return;
|
|
579
|
+
if (didRetry) return;
|
|
595
580
|
}
|
|
596
581
|
|
|
597
|
-
await this.#checkCompaction(msg);
|
|
598
|
-
|
|
599
582
|
// Check verification (if agent modified files without verifying)
|
|
600
583
|
if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
|
|
601
584
|
const didRemind = await this.#checkVerification();
|
|
@@ -711,16 +694,6 @@ export class AgentSession {
|
|
|
711
694
|
isError: event.isError ?? false,
|
|
712
695
|
};
|
|
713
696
|
await this.#extensionRunner.emit(extensionEvent);
|
|
714
|
-
} else if (event.type === "auto_compaction_start") {
|
|
715
|
-
await this.#extensionRunner.emit({ type: "auto_compaction_start", reason: event.reason });
|
|
716
|
-
} else if (event.type === "auto_compaction_end") {
|
|
717
|
-
await this.#extensionRunner.emit({
|
|
718
|
-
type: "auto_compaction_end",
|
|
719
|
-
result: event.result,
|
|
720
|
-
aborted: event.aborted,
|
|
721
|
-
willRetry: event.willRetry,
|
|
722
|
-
errorMessage: event.errorMessage,
|
|
723
|
-
});
|
|
724
697
|
} else if (event.type === "auto_retry_start") {
|
|
725
698
|
await this.#extensionRunner.emit({
|
|
726
699
|
type: "auto_retry_start",
|
|
@@ -940,11 +913,6 @@ export class AgentSession {
|
|
|
940
913
|
await this.setActiveToolsByName(nextActive);
|
|
941
914
|
}
|
|
942
915
|
|
|
943
|
-
/** Whether auto-compaction is currently running */
|
|
944
|
-
get isCompacting(): boolean {
|
|
945
|
-
return this.#autoCompactionAbortController !== undefined || this.#compactionAbortController !== undefined;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
916
|
/** All messages including custom types like BashExecutionMessage */
|
|
949
917
|
get messages(): AgentMessage[] {
|
|
950
918
|
return this.agent.state.messages;
|
|
@@ -1143,12 +1111,6 @@ export class AgentSession {
|
|
|
1143
1111
|
);
|
|
1144
1112
|
}
|
|
1145
1113
|
|
|
1146
|
-
// Check if we need to compact before sending (catches aborted responses)
|
|
1147
|
-
const lastAssistant = this.#findLastAssistantMessage();
|
|
1148
|
-
if (lastAssistant) {
|
|
1149
|
-
await this.#checkCompaction(lastAssistant, false);
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
1114
|
// Build messages array (custom messages if any, then user message)
|
|
1153
1115
|
const messages: AgentMessage[] = [];
|
|
1154
1116
|
|
|
@@ -1289,12 +1251,6 @@ export class AgentSession {
|
|
|
1289
1251
|
const result = await this.navigateTree(targetId, { summarize: options?.summarize });
|
|
1290
1252
|
return { cancelled: result.cancelled };
|
|
1291
1253
|
},
|
|
1292
|
-
compact: async instructionsOrOptions => {
|
|
1293
|
-
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
1294
|
-
const options =
|
|
1295
|
-
instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
|
|
1296
|
-
await this.compact(instructions, options);
|
|
1297
|
-
},
|
|
1298
1254
|
switchSession: async sessionPath => {
|
|
1299
1255
|
const success = await this.switchSession(sessionPath);
|
|
1300
1256
|
return { cancelled: !success };
|
|
@@ -1783,187 +1739,6 @@ export class AgentSession {
|
|
|
1783
1739
|
this.settings.set("interruptMode", mode);
|
|
1784
1740
|
}
|
|
1785
1741
|
|
|
1786
|
-
// =========================================================================
|
|
1787
|
-
// Compaction
|
|
1788
|
-
// =========================================================================
|
|
1789
|
-
|
|
1790
|
-
async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
1791
|
-
const branchEntries = this.sessionManager.getBranch();
|
|
1792
|
-
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
1793
|
-
if (result.prunedCount === 0) {
|
|
1794
|
-
return undefined;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
await this.sessionManager.rewriteEntries();
|
|
1798
|
-
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1799
|
-
this.agent.replaceMessages(sessionContext.messages);
|
|
1800
|
-
this.#model.closeCodexProviderSessionsForHistoryRewrite();
|
|
1801
|
-
return result;
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
/**
|
|
1805
|
-
* Manually compact the session context.
|
|
1806
|
-
* Aborts current agent operation first.
|
|
1807
|
-
* @param customInstructions Optional instructions for the compaction summary
|
|
1808
|
-
* @param options Optional callbacks for completion/error handling
|
|
1809
|
-
*/
|
|
1810
|
-
async compact(customInstructions?: string, options?: CompactOptions): Promise<CompactionResult> {
|
|
1811
|
-
this.#disconnectFromAgent();
|
|
1812
|
-
await this.abort();
|
|
1813
|
-
this.#compactionAbortController = new AbortController();
|
|
1814
|
-
|
|
1815
|
-
try {
|
|
1816
|
-
if (!this.model) {
|
|
1817
|
-
throw new Error("No model selected");
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
const compactionSettings = this.settings.getGroup("compaction");
|
|
1821
|
-
const compactionModel = this.model;
|
|
1822
|
-
const apiKey = await this.#model.registry.getApiKey(compactionModel, this.sessionId);
|
|
1823
|
-
if (!apiKey) {
|
|
1824
|
-
throw new Error(`No API key for ${compactionModel.provider}`);
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
const pathEntries = this.sessionManager.getBranch();
|
|
1828
|
-
|
|
1829
|
-
const preparation = prepareCompaction(pathEntries, compactionSettings);
|
|
1830
|
-
if (!preparation) {
|
|
1831
|
-
// Check why we can't compact
|
|
1832
|
-
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
1833
|
-
if (lastEntry?.type === "compaction") {
|
|
1834
|
-
throw new Error("Already compacted");
|
|
1835
|
-
}
|
|
1836
|
-
throw new Error("Nothing to compact (session too small)");
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
let hookCompaction: CompactionResult | undefined;
|
|
1840
|
-
let fromExtension = false;
|
|
1841
|
-
let hookContext: string[] | undefined;
|
|
1842
|
-
let hookPrompt: string | undefined;
|
|
1843
|
-
let preserveData: Record<string, unknown> | undefined;
|
|
1844
|
-
|
|
1845
|
-
if (this.#extensionRunner?.hasHandlers("session_before_compact")) {
|
|
1846
|
-
const result = (await this.#extensionRunner.emit({
|
|
1847
|
-
type: "session_before_compact",
|
|
1848
|
-
preparation,
|
|
1849
|
-
branchEntries: pathEntries,
|
|
1850
|
-
customInstructions,
|
|
1851
|
-
signal: this.#compactionAbortController.signal,
|
|
1852
|
-
})) as SessionBeforeCompactResult | undefined;
|
|
1853
|
-
|
|
1854
|
-
if (result?.cancel) {
|
|
1855
|
-
throw new Error("Compaction cancelled");
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
if (result?.compaction) {
|
|
1859
|
-
hookCompaction = result.compaction;
|
|
1860
|
-
fromExtension = true;
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
if (!hookCompaction && this.#extensionRunner?.hasHandlers("session.compacting")) {
|
|
1865
|
-
const compactMessages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
1866
|
-
const result = (await this.#extensionRunner.emit({
|
|
1867
|
-
type: "session.compacting",
|
|
1868
|
-
sessionId: this.sessionId,
|
|
1869
|
-
messages: compactMessages,
|
|
1870
|
-
})) as { context?: string[]; prompt?: string; preserveData?: Record<string, unknown> } | undefined;
|
|
1871
|
-
|
|
1872
|
-
hookContext = result?.context;
|
|
1873
|
-
hookPrompt = result?.prompt;
|
|
1874
|
-
preserveData = result?.preserveData;
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
let summary: string;
|
|
1878
|
-
let shortSummary: string | undefined;
|
|
1879
|
-
let firstKeptEntryId: string;
|
|
1880
|
-
let tokensBefore: number;
|
|
1881
|
-
let details: unknown;
|
|
1882
|
-
|
|
1883
|
-
if (hookCompaction) {
|
|
1884
|
-
// Extension provided compaction content
|
|
1885
|
-
summary = hookCompaction.summary;
|
|
1886
|
-
shortSummary = hookCompaction.shortSummary;
|
|
1887
|
-
firstKeptEntryId = hookCompaction.firstKeptEntryId;
|
|
1888
|
-
tokensBefore = hookCompaction.tokensBefore;
|
|
1889
|
-
details = hookCompaction.details;
|
|
1890
|
-
preserveData ??= hookCompaction.preserveData;
|
|
1891
|
-
} else {
|
|
1892
|
-
// Generate compaction result
|
|
1893
|
-
const result = await compact(
|
|
1894
|
-
preparation,
|
|
1895
|
-
compactionModel,
|
|
1896
|
-
apiKey,
|
|
1897
|
-
customInstructions,
|
|
1898
|
-
this.#compactionAbortController.signal,
|
|
1899
|
-
{ promptOverride: hookPrompt, extraContext: hookContext },
|
|
1900
|
-
);
|
|
1901
|
-
summary = result.summary;
|
|
1902
|
-
shortSummary = result.shortSummary;
|
|
1903
|
-
firstKeptEntryId = result.firstKeptEntryId;
|
|
1904
|
-
tokensBefore = result.tokensBefore;
|
|
1905
|
-
details = result.details;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
if (this.#compactionAbortController.signal.aborted) {
|
|
1909
|
-
throw new Error("Compaction cancelled");
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
this.sessionManager.appendCompaction(
|
|
1913
|
-
summary,
|
|
1914
|
-
shortSummary,
|
|
1915
|
-
firstKeptEntryId,
|
|
1916
|
-
tokensBefore,
|
|
1917
|
-
details,
|
|
1918
|
-
fromExtension,
|
|
1919
|
-
preserveData,
|
|
1920
|
-
);
|
|
1921
|
-
const newEntries = this.sessionManager.getEntries();
|
|
1922
|
-
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1923
|
-
this.agent.replaceMessages(sessionContext.messages);
|
|
1924
|
-
this.#model.closeCodexProviderSessionsForHistoryRewrite();
|
|
1925
|
-
|
|
1926
|
-
// Get the saved compaction entry for the hook
|
|
1927
|
-
const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
|
|
1928
|
-
| CompactionEntry
|
|
1929
|
-
| undefined;
|
|
1930
|
-
|
|
1931
|
-
if (this.#extensionRunner && savedCompactionEntry) {
|
|
1932
|
-
await this.#extensionRunner.emit({
|
|
1933
|
-
type: "session_compact",
|
|
1934
|
-
compactionEntry: savedCompactionEntry,
|
|
1935
|
-
fromExtension,
|
|
1936
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
const compactionResult: CompactionResult = {
|
|
1940
|
-
summary,
|
|
1941
|
-
shortSummary,
|
|
1942
|
-
firstKeptEntryId,
|
|
1943
|
-
tokensBefore,
|
|
1944
|
-
details,
|
|
1945
|
-
preserveData,
|
|
1946
|
-
};
|
|
1947
|
-
options?.onComplete?.(compactionResult);
|
|
1948
|
-
return compactionResult;
|
|
1949
|
-
} catch (error) {
|
|
1950
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
1951
|
-
options?.onError?.(err);
|
|
1952
|
-
throw error;
|
|
1953
|
-
} finally {
|
|
1954
|
-
this.#compactionAbortController = undefined;
|
|
1955
|
-
this.#reconnectToAgent();
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
/**
|
|
1960
|
-
* Cancel in-progress compaction (manual or auto).
|
|
1961
|
-
*/
|
|
1962
|
-
abortCompaction(): void {
|
|
1963
|
-
this.#compactionAbortController?.abort();
|
|
1964
|
-
this.#autoCompactionAbortController?.abort();
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
1742
|
/**
|
|
1968
1743
|
* Cancel in-progress branch summarization.
|
|
1969
1744
|
*/
|
|
@@ -2092,6 +1867,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2092
1867
|
this.#followUpMessages = [];
|
|
2093
1868
|
this.#pendingNextTurnMessages = [];
|
|
2094
1869
|
this.#todoReminderCount = 0;
|
|
1870
|
+
this.#contextWarningEmitted = false;
|
|
2095
1871
|
|
|
2096
1872
|
// Inject the handoff document as a custom message
|
|
2097
1873
|
const handoffContent = `<handoff-context thread="${parentThreadId}">\n${handoffText}\n</handoff-context>\n\nThe above is a handoff document from thread \`${parentThreadId}\`. Use this context to continue the work seamlessly. If you need additional details not covered above, use \`read_thread("${parentThreadId}", "your specific question")\` to query the original session.`;
|
|
@@ -2107,79 +1883,6 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2107
1883
|
}
|
|
2108
1884
|
}
|
|
2109
1885
|
|
|
2110
|
-
/**
|
|
2111
|
-
* Check if compaction or context promotion is needed and run it.
|
|
2112
|
-
* Called after agent_end and before prompt submission.
|
|
2113
|
-
*
|
|
2114
|
-
* Three cases (in order):
|
|
2115
|
-
* 1. Overflow + promotion: promote to larger model, retry without compacting
|
|
2116
|
-
* 2. Overflow + no promotion target: compact, auto-retry on same model
|
|
2117
|
-
* 3. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
|
|
2118
|
-
*
|
|
2119
|
-
* @param assistantMessage The assistant message to check
|
|
2120
|
-
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
2121
|
-
*/
|
|
2122
|
-
async #checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<void> {
|
|
2123
|
-
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
2124
|
-
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return;
|
|
2125
|
-
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2126
|
-
// Skip overflow check if the message came from a different model.
|
|
2127
|
-
// This handles the case where user switched from a smaller-context model (e.g. opus)
|
|
2128
|
-
// to a larger-context model (e.g. codex) - the overflow error from the old model
|
|
2129
|
-
// shouldn't trigger compaction for the new model.
|
|
2130
|
-
const sameModel =
|
|
2131
|
-
this.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;
|
|
2132
|
-
// This handles the case where an error was kept after compaction (in the "kept" region).
|
|
2133
|
-
// The error shouldn't trigger another compaction since we already compacted.
|
|
2134
|
-
// Example: opus fails \u2192 switch to codex \u2192 compact \u2192 switch back to opus \u2192 opus error
|
|
2135
|
-
// is still in context but shouldn't trigger compaction again.
|
|
2136
|
-
const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
2137
|
-
const errorIsFromBeforeCompaction =
|
|
2138
|
-
compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
|
|
2139
|
-
if (sameModel && !errorIsFromBeforeCompaction && isContextOverflow(assistantMessage, contextWindow)) {
|
|
2140
|
-
// Remove the error message from agent state (it IS saved to session for history,
|
|
2141
|
-
// but we don't want it in context for the retry)
|
|
2142
|
-
const messages = this.agent.state.messages;
|
|
2143
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2144
|
-
this.agent.replaceMessages(messages.slice(0, -1));
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
// Try context promotion first \u2014 switch to a larger model and retry without compacting
|
|
2148
|
-
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
2149
|
-
if (promoted) {
|
|
2150
|
-
// Retry on the promoted (larger) model without compacting
|
|
2151
|
-
setTimeout(() => {
|
|
2152
|
-
this.agent.continue().catch(() => {});
|
|
2153
|
-
}, 100);
|
|
2154
|
-
return;
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
// No promotion target available \u2014 fall through to compaction
|
|
2158
|
-
const compactionSettings = this.settings.getGroup("compaction");
|
|
2159
|
-
if (compactionSettings.enabled) {
|
|
2160
|
-
await this.#runAutoCompaction("overflow", true);
|
|
2161
|
-
}
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
const compactionSettings = this.settings.getGroup("compaction");
|
|
2165
|
-
if (!compactionSettings.enabled) return;
|
|
2166
|
-
|
|
2167
|
-
// Case 2: Threshold - turn succeeded but context is getting large
|
|
2168
|
-
// Skip if this was an error (non-overflow errors don't have usage data)
|
|
2169
|
-
if (assistantMessage.stopReason === "error") return;
|
|
2170
|
-
const pruneResult = await this.#pruneToolOutputs();
|
|
2171
|
-
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
2172
|
-
if (pruneResult) {
|
|
2173
|
-
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
2174
|
-
}
|
|
2175
|
-
if (shouldCompact(contextTokens, contextWindow, compactionSettings)) {
|
|
2176
|
-
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
2177
|
-
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
2178
|
-
if (!promoted) {
|
|
2179
|
-
await this.#runAutoCompaction("threshold", false);
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
1886
|
/**
|
|
2184
1887
|
* Check if agent stopped after modifying files without running verification.
|
|
2185
1888
|
* If so, inject a reminder to verify and continue the conversation.
|
|
@@ -2319,368 +2022,104 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2319
2022
|
this.agent.continue().catch(() => {});
|
|
2320
2023
|
}
|
|
2321
2024
|
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
*/
|
|
2326
|
-
async #tryContextPromotion(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
2327
|
-
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
2328
|
-
if (!promotionSettings.enabled) return false;
|
|
2329
|
-
const currentModel = this.model;
|
|
2330
|
-
if (!currentModel) return false;
|
|
2331
|
-
if (assistantMessage.provider !== currentModel.provider || assistantMessage.model !== currentModel.id)
|
|
2332
|
-
return false;
|
|
2333
|
-
const contextWindow = currentModel.contextWindow ?? 0;
|
|
2334
|
-
if (contextWindow <= 0) return false;
|
|
2335
|
-
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
|
|
2336
|
-
if (!targetModel) return false;
|
|
2337
|
-
|
|
2338
|
-
try {
|
|
2339
|
-
await this.setModelTemporary(targetModel);
|
|
2340
|
-
logger.debug("Context promotion switched model on overflow", {
|
|
2341
|
-
from: `${currentModel.provider}/${currentModel.id}`,
|
|
2342
|
-
to: `${targetModel.provider}/${targetModel.id}`,
|
|
2343
|
-
});
|
|
2344
|
-
return true;
|
|
2345
|
-
} catch (error) {
|
|
2346
|
-
logger.warn("Context promotion failed", {
|
|
2347
|
-
from: `${currentModel.provider}/${currentModel.id}`,
|
|
2348
|
-
to: `${targetModel.provider}/${targetModel.id}`,
|
|
2349
|
-
error: String(error),
|
|
2350
|
-
});
|
|
2351
|
-
return false;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
async #resolveContextPromotionTarget(currentModel: Model, contextWindow: number): Promise<Model | undefined> {
|
|
2356
|
-
const availableModels = this.#model.registry.getAvailable();
|
|
2357
|
-
if (availableModels.length === 0) return undefined;
|
|
2025
|
+
// =========================================================================
|
|
2026
|
+
// Auto-Handoff
|
|
2027
|
+
// =========================================================================
|
|
2358
2028
|
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
if (
|
|
2362
|
-
if (candidate.contextWindow <= contextWindow) return undefined;
|
|
2363
|
-
const apiKey = await this.#model.registry.getApiKey(candidate, this.sessionId);
|
|
2364
|
-
if (!apiKey) return undefined;
|
|
2365
|
-
return candidate;
|
|
2366
|
-
}
|
|
2367
|
-
/**
|
|
2368
|
-
* Internal: Run auto-compaction with events.
|
|
2369
|
-
*/
|
|
2370
|
-
async #runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
|
|
2371
|
-
const compactionSettings = this.settings.getGroup("compaction");
|
|
2029
|
+
async #checkAutoHandoff(message: AssistantMessage): Promise<boolean> {
|
|
2030
|
+
const handoffSettings = this.settings.getGroup("autoHandoff");
|
|
2031
|
+
if (!handoffSettings.enabled) return false;
|
|
2372
2032
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
if (
|
|
2376
|
-
this.#
|
|
2033
|
+
// Check if this was a context overflow error
|
|
2034
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2035
|
+
if (message.stopReason === "error" && isContextOverflow(message, contextWindow)) {
|
|
2036
|
+
return this.#handleContextOverflow();
|
|
2377
2037
|
}
|
|
2378
|
-
this.#autoCompactionAbortController = new AbortController();
|
|
2379
2038
|
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
type: "auto_compaction_end",
|
|
2384
|
-
result: undefined,
|
|
2385
|
-
aborted: false,
|
|
2386
|
-
willRetry: false,
|
|
2387
|
-
});
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2039
|
+
// Skip threshold-based handoff if this turn ended with an error
|
|
2040
|
+
// (e.g., 429 rate limit) — let retry logic handle those
|
|
2041
|
+
if (message.stopReason === "error") return false;
|
|
2390
2042
|
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
type: "auto_compaction_end",
|
|
2395
|
-
result: undefined,
|
|
2396
|
-
aborted: false,
|
|
2397
|
-
willRetry: false,
|
|
2398
|
-
});
|
|
2399
|
-
return;
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
const pathEntries = this.sessionManager.getBranch();
|
|
2403
|
-
|
|
2404
|
-
const preparation = prepareCompaction(pathEntries, compactionSettings);
|
|
2405
|
-
if (!preparation) {
|
|
2406
|
-
await this.#emitSessionEvent({
|
|
2407
|
-
type: "auto_compaction_end",
|
|
2408
|
-
result: undefined,
|
|
2409
|
-
aborted: false,
|
|
2410
|
-
willRetry: false,
|
|
2411
|
-
});
|
|
2412
|
-
return;
|
|
2413
|
-
}
|
|
2043
|
+
// Check context usage percentage
|
|
2044
|
+
const usage = this.getContextUsage();
|
|
2045
|
+
if (!usage || usage.percent == null) return false;
|
|
2414
2046
|
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
type: "session_before_compact",
|
|
2424
|
-
preparation,
|
|
2425
|
-
branchEntries: pathEntries,
|
|
2426
|
-
customInstructions: undefined,
|
|
2427
|
-
signal: this.#autoCompactionAbortController.signal,
|
|
2428
|
-
})) as SessionBeforeCompactResult | undefined;
|
|
2429
|
-
|
|
2430
|
-
if (hookResult?.cancel) {
|
|
2431
|
-
await this.#emitSessionEvent({
|
|
2432
|
-
type: "auto_compaction_end",
|
|
2433
|
-
result: undefined,
|
|
2434
|
-
aborted: true,
|
|
2435
|
-
willRetry: false,
|
|
2436
|
-
});
|
|
2437
|
-
return;
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
|
-
if (hookResult?.compaction) {
|
|
2441
|
-
hookCompaction = hookResult.compaction;
|
|
2442
|
-
fromExtension = true;
|
|
2047
|
+
// Auto-handoff at high threshold
|
|
2048
|
+
if (usage.percent >= handoffSettings.handoffThreshold) {
|
|
2049
|
+
await this.#emitSessionEvent({ type: "auto_handoff_start", percent: usage.percent });
|
|
2050
|
+
try {
|
|
2051
|
+
const result = await this.handoff();
|
|
2052
|
+
await this.#emitSessionEvent({ type: "auto_handoff_end", success: !!result });
|
|
2053
|
+
if (result) {
|
|
2054
|
+
this.agent.continue().catch(() => {});
|
|
2443
2055
|
}
|
|
2056
|
+
return !!result;
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
// handoff() failed (likely context too full to generate doc)
|
|
2059
|
+
// End the current handoff event before falling back to overflow handler
|
|
2060
|
+
await this.#emitSessionEvent({ type: "auto_handoff_end", success: false, error: String(err) });
|
|
2061
|
+
return this.#handleContextOverflow();
|
|
2444
2062
|
}
|
|
2063
|
+
}
|
|
2445
2064
|
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
preserveData = result?.preserveData;
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
let summary: string;
|
|
2460
|
-
let shortSummary: string | undefined;
|
|
2461
|
-
let firstKeptEntryId: string;
|
|
2462
|
-
let tokensBefore: number;
|
|
2463
|
-
let details: unknown;
|
|
2464
|
-
|
|
2465
|
-
if (hookCompaction) {
|
|
2466
|
-
// Extension provided compaction content
|
|
2467
|
-
summary = hookCompaction.summary;
|
|
2468
|
-
shortSummary = hookCompaction.shortSummary;
|
|
2469
|
-
firstKeptEntryId = hookCompaction.firstKeptEntryId;
|
|
2470
|
-
tokensBefore = hookCompaction.tokensBefore;
|
|
2471
|
-
details = hookCompaction.details;
|
|
2472
|
-
preserveData ??= hookCompaction.preserveData;
|
|
2473
|
-
} else {
|
|
2474
|
-
const candidates = this.#model.getCompactionModelCandidates(availableModels);
|
|
2475
|
-
const retrySettings = this.settings.getGroup("retry");
|
|
2476
|
-
let compactResult: CompactionResult | undefined;
|
|
2477
|
-
let lastError: unknown;
|
|
2478
|
-
|
|
2479
|
-
for (const candidate of candidates) {
|
|
2480
|
-
const apiKey = await this.#model.registry.getApiKey(candidate, this.sessionId);
|
|
2481
|
-
if (!apiKey) continue;
|
|
2482
|
-
|
|
2483
|
-
let attempt = 0;
|
|
2484
|
-
while (true) {
|
|
2485
|
-
try {
|
|
2486
|
-
compactResult = await compact(
|
|
2487
|
-
preparation,
|
|
2488
|
-
candidate,
|
|
2489
|
-
apiKey,
|
|
2490
|
-
undefined,
|
|
2491
|
-
this.#autoCompactionAbortController.signal,
|
|
2492
|
-
{ promptOverride: hookPrompt, extraContext: hookContext },
|
|
2493
|
-
);
|
|
2494
|
-
break;
|
|
2495
|
-
} catch (error) {
|
|
2496
|
-
if (this.#autoCompactionAbortController.signal.aborted) {
|
|
2497
|
-
throw error;
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2501
|
-
const retryAfterMs = parseRetryAfterMs(message);
|
|
2502
|
-
const shouldRetry =
|
|
2503
|
-
retrySettings.enabled &&
|
|
2504
|
-
attempt < retrySettings.maxRetries &&
|
|
2505
|
-
(retryAfterMs !== undefined || isRetryableErrorMessage(message));
|
|
2506
|
-
if (!shouldRetry) {
|
|
2507
|
-
lastError = error;
|
|
2508
|
-
break;
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
|
|
2512
|
-
const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
|
|
2513
|
-
|
|
2514
|
-
// If retry delay is too long (>30s), try next candidate instead of waiting
|
|
2515
|
-
const maxAcceptableDelayMs = 30_000;
|
|
2516
|
-
if (delayMs > maxAcceptableDelayMs) {
|
|
2517
|
-
const hasMoreCandidates = candidates.indexOf(candidate) < candidates.length - 1;
|
|
2518
|
-
if (hasMoreCandidates) {
|
|
2519
|
-
logger.warn("Auto-compaction retry delay too long, trying next model", {
|
|
2520
|
-
delayMs,
|
|
2521
|
-
retryAfterMs,
|
|
2522
|
-
error: message,
|
|
2523
|
-
model: `${candidate.provider}/${candidate.id}`,
|
|
2524
|
-
});
|
|
2525
|
-
lastError = error;
|
|
2526
|
-
break; // Exit retry loop, continue to next candidate
|
|
2527
|
-
}
|
|
2528
|
-
// No more candidates - we have to wait
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
attempt++;
|
|
2532
|
-
logger.warn("Auto-compaction failed, retrying", {
|
|
2533
|
-
attempt,
|
|
2534
|
-
maxRetries: retrySettings.maxRetries,
|
|
2535
|
-
delayMs,
|
|
2536
|
-
retryAfterMs,
|
|
2537
|
-
error: message,
|
|
2538
|
-
model: `${candidate.provider}/${candidate.id}`,
|
|
2539
|
-
});
|
|
2540
|
-
await abortableSleep(delayMs, this.#autoCompactionAbortController.signal);
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
if (compactResult) {
|
|
2545
|
-
break;
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2065
|
+
// Warning at lower threshold (once per session)
|
|
2066
|
+
if (usage.percent >= handoffSettings.warningThreshold && !this.#contextWarningEmitted) {
|
|
2067
|
+
this.#contextWarningEmitted = true;
|
|
2068
|
+
await this.#emitSessionEvent({
|
|
2069
|
+
type: "context_warning",
|
|
2070
|
+
percent: usage.percent,
|
|
2071
|
+
tokens: usage.tokens ?? 0,
|
|
2072
|
+
contextWindow: usage.contextWindow,
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2548
2075
|
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
throw lastError;
|
|
2552
|
-
}
|
|
2553
|
-
throw new Error("Compaction failed: no available model");
|
|
2554
|
-
}
|
|
2076
|
+
return false;
|
|
2077
|
+
}
|
|
2555
2078
|
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2079
|
+
async #handleContextOverflow(): Promise<boolean> {
|
|
2080
|
+
await this.#emitSessionEvent({ type: "auto_handoff_start", percent: 100 });
|
|
2081
|
+
try {
|
|
2082
|
+
// Can't prompt agent (context full), build lightweight handoff from session state
|
|
2083
|
+
const parentThreadId = this.sessionManager.getSessionId();
|
|
2084
|
+
await this.sessionManager.flush();
|
|
2085
|
+
await this.sessionManager.newSession({ parentSession: parentThreadId });
|
|
2086
|
+
this.agent.reset();
|
|
2087
|
+
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
2088
|
+
this.#steeringMessages = [];
|
|
2089
|
+
this.#followUpMessages = [];
|
|
2090
|
+
this.#pendingNextTurnMessages = [];
|
|
2091
|
+
this.#todoReminderCount = 0;
|
|
2092
|
+
this.#contextWarningEmitted = false;
|
|
2562
2093
|
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
result: undefined,
|
|
2567
|
-
aborted: true,
|
|
2568
|
-
willRetry: false,
|
|
2569
|
-
});
|
|
2570
|
-
return;
|
|
2571
|
-
}
|
|
2094
|
+
// Inject a minimal handoff message pointing back to the parent thread
|
|
2095
|
+
const handoffContent = `<handoff-context thread="${parentThreadId}">\nContext window reached capacity. This session was automatically created to continue the work.\n\nUse \`read_thread("${parentThreadId}", "your specific question")\` to retrieve context from the previous session as needed.\n</handoff-context>\n\nThe previous session ran out of context space. Read the parent thread to understand what was being worked on and continue.`;
|
|
2096
|
+
this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true);
|
|
2572
2097
|
|
|
2573
|
-
|
|
2574
|
-
summary,
|
|
2575
|
-
shortSummary,
|
|
2576
|
-
firstKeptEntryId,
|
|
2577
|
-
tokensBefore,
|
|
2578
|
-
details,
|
|
2579
|
-
fromExtension,
|
|
2580
|
-
preserveData,
|
|
2581
|
-
);
|
|
2582
|
-
const newEntries = this.sessionManager.getEntries();
|
|
2098
|
+
// Rebuild agent messages from session
|
|
2583
2099
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
2584
2100
|
this.agent.replaceMessages(sessionContext.messages);
|
|
2585
|
-
this.#model.closeCodexProviderSessionsForHistoryRewrite();
|
|
2586
|
-
|
|
2587
|
-
// Get the saved compaction entry for the hook
|
|
2588
|
-
const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
|
|
2589
|
-
| CompactionEntry
|
|
2590
|
-
| undefined;
|
|
2591
|
-
|
|
2592
|
-
if (this.#extensionRunner && savedCompactionEntry) {
|
|
2593
|
-
await this.#extensionRunner.emit({
|
|
2594
|
-
type: "session_compact",
|
|
2595
|
-
compactionEntry: savedCompactionEntry,
|
|
2596
|
-
fromExtension,
|
|
2597
|
-
});
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
const result: CompactionResult = {
|
|
2601
|
-
summary,
|
|
2602
|
-
shortSummary,
|
|
2603
|
-
firstKeptEntryId,
|
|
2604
|
-
tokensBefore,
|
|
2605
|
-
details,
|
|
2606
|
-
preserveData,
|
|
2607
|
-
};
|
|
2608
|
-
await this.#emitSessionEvent({ type: "auto_compaction_end", result, aborted: false, willRetry });
|
|
2609
|
-
|
|
2610
|
-
if (!willRetry && compactionSettings.autoContinue !== false) {
|
|
2611
|
-
await this.prompt("Continue if you have next steps.", {
|
|
2612
|
-
expandPromptTemplates: false,
|
|
2613
|
-
synthetic: true,
|
|
2614
|
-
});
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
if (willRetry) {
|
|
2618
|
-
const messages = this.agent.state.messages;
|
|
2619
|
-
const lastMsg = messages[messages.length - 1];
|
|
2620
|
-
if (lastMsg?.role === "assistant" && (lastMsg as AssistantMessage).stopReason === "error") {
|
|
2621
|
-
this.agent.replaceMessages(messages.slice(0, -1));
|
|
2622
|
-
}
|
|
2623
2101
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
this.agent.continue().catch(() => {});
|
|
2632
|
-
}, 100);
|
|
2633
|
-
}
|
|
2634
|
-
} catch (error) {
|
|
2635
|
-
if (this.#autoCompactionAbortController?.signal.aborted) {
|
|
2636
|
-
await this.#emitSessionEvent({
|
|
2637
|
-
type: "auto_compaction_end",
|
|
2638
|
-
result: undefined,
|
|
2639
|
-
aborted: true,
|
|
2640
|
-
willRetry: false,
|
|
2641
|
-
});
|
|
2642
|
-
return;
|
|
2643
|
-
}
|
|
2644
|
-
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
2645
|
-
await this.#emitSessionEvent({
|
|
2646
|
-
type: "auto_compaction_end",
|
|
2647
|
-
result: undefined,
|
|
2648
|
-
aborted: false,
|
|
2649
|
-
willRetry: false,
|
|
2650
|
-
errorMessage:
|
|
2651
|
-
reason === "overflow"
|
|
2652
|
-
? `Context overflow recovery failed: ${errorMessage}`
|
|
2653
|
-
: `Auto-compaction failed: ${errorMessage}`,
|
|
2654
|
-
});
|
|
2655
|
-
} finally {
|
|
2656
|
-
this.#autoCompactionAbortController = undefined;
|
|
2102
|
+
await this.#emitSessionEvent({ type: "auto_handoff_end", success: true });
|
|
2103
|
+
this.agent.continue().catch(() => {});
|
|
2104
|
+
return true;
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
logger.error("Auto-handoff overflow fallback failed", { error: String(err) });
|
|
2107
|
+
await this.#emitSessionEvent({ type: "auto_handoff_end", success: false, error: String(err) });
|
|
2108
|
+
return false;
|
|
2657
2109
|
}
|
|
2658
2110
|
}
|
|
2659
|
-
|
|
2660
|
-
/**
|
|
2661
|
-
* Toggle auto-compaction setting.
|
|
2662
|
-
*/
|
|
2663
|
-
setAutoCompactionEnabled(enabled: boolean): void {
|
|
2664
|
-
this.settings.set("compaction.enabled", enabled);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
/** Whether auto-compaction is enabled */
|
|
2668
|
-
get autoCompactionEnabled(): boolean {
|
|
2669
|
-
return this.settings.get("compaction.enabled");
|
|
2670
|
-
}
|
|
2671
|
-
|
|
2672
2111
|
// =========================================================================
|
|
2673
2112
|
// Auto-Retry
|
|
2674
2113
|
// =========================================================================
|
|
2675
2114
|
|
|
2676
2115
|
/**
|
|
2677
2116
|
* Check if an error is retryable (overloaded, rate limit, server errors).
|
|
2678
|
-
* Context overflow errors are NOT retryable
|
|
2117
|
+
* Context overflow errors are NOT retryable.
|
|
2679
2118
|
*/
|
|
2680
2119
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
2681
2120
|
if (message.stopReason !== "error" || !message.errorMessage) return false;
|
|
2682
2121
|
|
|
2683
|
-
// Context overflow is
|
|
2122
|
+
// Context overflow is not retryable
|
|
2684
2123
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2685
2124
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
2686
2125
|
|
|
@@ -3397,7 +2836,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3397
2836
|
}
|
|
3398
2837
|
|
|
3399
2838
|
getContextUsage(): ContextUsage | undefined {
|
|
3400
|
-
return sessionStats.getContextUsage(this.model, this.messages
|
|
2839
|
+
return sessionStats.getContextUsage(this.model, this.messages);
|
|
3401
2840
|
}
|
|
3402
2841
|
|
|
3403
2842
|
async fetchUsageReports(): Promise<UsageReport[] | null> {
|