@kolisachint/hoocode-agent 0.4.108 → 0.4.109

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 (57) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/core/agent-session-compaction.d.ts +79 -0
  3. package/dist/core/agent-session-compaction.d.ts.map +1 -0
  4. package/dist/core/agent-session-compaction.js +346 -0
  5. package/dist/core/agent-session-compaction.js.map +1 -0
  6. package/dist/core/agent-session-retry.d.ts +76 -0
  7. package/dist/core/agent-session-retry.d.ts.map +1 -0
  8. package/dist/core/agent-session-retry.js +192 -0
  9. package/dist/core/agent-session-retry.js.map +1 -0
  10. package/dist/core/agent-session-skills.d.ts +34 -0
  11. package/dist/core/agent-session-skills.d.ts.map +1 -0
  12. package/dist/core/agent-session-skills.js +52 -0
  13. package/dist/core/agent-session-skills.js.map +1 -0
  14. package/dist/core/agent-session-stats.d.ts +74 -0
  15. package/dist/core/agent-session-stats.d.ts.map +1 -0
  16. package/dist/core/agent-session-stats.js +187 -0
  17. package/dist/core/agent-session-stats.js.map +1 -0
  18. package/dist/core/agent-session-tree-navigation.d.ts +69 -0
  19. package/dist/core/agent-session-tree-navigation.d.ts.map +1 -0
  20. package/dist/core/agent-session-tree-navigation.js +198 -0
  21. package/dist/core/agent-session-tree-navigation.js.map +1 -0
  22. package/dist/core/agent-session.d.ts +10 -66
  23. package/dist/core/agent-session.d.ts.map +1 -1
  24. package/dist/core/agent-session.js +96 -806
  25. package/dist/core/agent-session.js.map +1 -1
  26. package/dist/core/context-files.d.ts +25 -0
  27. package/dist/core/context-files.d.ts.map +1 -0
  28. package/dist/core/context-files.js +97 -0
  29. package/dist/core/context-files.js.map +1 -0
  30. package/dist/core/package-manager.d.ts.map +1 -1
  31. package/dist/core/package-manager.js +3 -519
  32. package/dist/core/package-manager.js.map +1 -1
  33. package/dist/core/package-resource-discovery.d.ts +62 -0
  34. package/dist/core/package-resource-discovery.d.ts.map +1 -0
  35. package/dist/core/package-resource-discovery.js +530 -0
  36. package/dist/core/package-resource-discovery.js.map +1 -0
  37. package/dist/core/resource-loader.d.ts +1 -10
  38. package/dist/core/resource-loader.d.ts.map +1 -1
  39. package/dist/core/resource-loader.js +4 -83
  40. package/dist/core/resource-loader.js.map +1 -1
  41. package/dist/core/settings-manager.d.ts +5 -140
  42. package/dist/core/settings-manager.d.ts.map +1 -1
  43. package/dist/core/settings-manager.js +4 -81
  44. package/dist/core/settings-manager.js.map +1 -1
  45. package/dist/core/settings-storage.d.ts +29 -0
  46. package/dist/core/settings-storage.d.ts.map +1 -0
  47. package/dist/core/settings-storage.js +90 -0
  48. package/dist/core/settings-storage.js.map +1 -0
  49. package/dist/core/settings-types.d.ts +128 -0
  50. package/dist/core/settings-types.d.ts.map +1 -0
  51. package/dist/core/settings-types.js +9 -0
  52. package/dist/core/settings-types.js.map +1 -0
  53. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  54. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  55. package/examples/extensions/sandbox/package.json +1 -1
  56. package/examples/extensions/with-deps/package.json +1 -1
  57. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.109] - 2026-07-03
4
+
3
5
  ## [0.4.108] - 2026-07-03
4
6
 
5
7
  ## [0.4.107] - 2026-07-02
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Compaction controller for AgentSession.
3
+ *
4
+ * Owns manual compaction (the /compact flow) and automatic compaction (overflow
5
+ * recovery and threshold-triggered). It runs the shared apply pipeline — the
6
+ * `session_before_compact` extension hook, summary generation, persistence,
7
+ * agent-context refresh, and the `session_compact` event — and manages the
8
+ * abort controllers and one-shot overflow-recovery guard. Extracted from
9
+ * agent-session.ts behind a narrow CompactionControllerDeps interface.
10
+ */
11
+ import type { AgentMessage, CompactionResult, ThinkingLevel } from "@kolisachint/hoocode-agent-core";
12
+ import type { AssistantMessage, Model } from "@kolisachint/hoocode-ai";
13
+ import type { AgentSessionEvent } from "./agent-session.js";
14
+ import type { ExtensionRunner } from "./extensions/index.js";
15
+ import type { ModelRegistry } from "./model-registry.js";
16
+ import type { SessionManager } from "./session-manager.js";
17
+ import type { SettingsManager } from "./settings-manager.js";
18
+ /** Narrow dependencies the compaction controller needs from AgentSession. */
19
+ export interface CompactionControllerDeps {
20
+ sessionManager: SessionManager;
21
+ settingsManager: SettingsManager;
22
+ modelRegistry: ModelRegistry;
23
+ getModel(): Model<any> | undefined;
24
+ getThinkingLevel(): ThinkingLevel;
25
+ /** Read at call time; the extension runner is swapped on reload. */
26
+ getExtensionRunner(): ExtensionRunner;
27
+ getAgentMessages(): AgentMessage[];
28
+ setAgentMessages(messages: AgentMessage[]): void;
29
+ getRequiredRequestAuth(model: Model<any>): Promise<{
30
+ apiKey: string;
31
+ headers?: Record<string, string>;
32
+ }>;
33
+ emit(event: AgentSessionEvent): void;
34
+ disconnectFromAgent(): void;
35
+ reconnectToAgent(): void;
36
+ /** Abort the current agent operation and wait for idle (AgentSession.abort). */
37
+ abortSession(): Promise<void>;
38
+ /** Fire-and-forget continue() on the agent. */
39
+ continueAgent(): void;
40
+ hasQueuedMessages(): boolean;
41
+ }
42
+ export declare class CompactionController {
43
+ private readonly deps;
44
+ private _compactionAbortController;
45
+ private _autoCompactionAbortController;
46
+ private _overflowRecoveryAttempted;
47
+ constructor(deps: CompactionControllerDeps);
48
+ /** Whether manual or auto compaction is currently running */
49
+ get isCompacting(): boolean;
50
+ /** Whether auto-compaction is enabled */
51
+ get autoCompactionEnabled(): boolean;
52
+ /** Toggle auto-compaction setting. */
53
+ setAutoCompactionEnabled(enabled: boolean): void;
54
+ /** Clear the one-shot overflow-recovery guard (on new user input / successful response). */
55
+ resetOverflowRecovery(): void;
56
+ /** Cancel in-progress compaction (manual or auto). */
57
+ abortCompaction(): void;
58
+ private _applyCompaction;
59
+ /**
60
+ * Manually compact the session context.
61
+ * Aborts current agent operation first.
62
+ * @param customInstructions Optional instructions for the compaction summary
63
+ */
64
+ compact(customInstructions?: string): Promise<CompactionResult>;
65
+ /**
66
+ * Check if compaction is needed and run it.
67
+ * Called after agent_end and before prompt submission.
68
+ *
69
+ * Two cases:
70
+ * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry
71
+ * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
72
+ *
73
+ * @param assistantMessage The assistant message to check
74
+ * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
75
+ */
76
+ checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck?: boolean): Promise<void>;
77
+ private _runAutoCompaction;
78
+ }
79
+ //# sourceMappingURL=agent-session-compaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-compaction.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-compaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACX,YAAY,EAEZ,gBAAgB,EAChB,aAAa,EACb,MAAM,iCAAiC,CAAC;AAQzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAEvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,KAAK,EAAE,eAAe,EAA8B,MAAM,uBAAuB,CAAC;AACzF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAiC,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE1F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,6EAA6E;AAC7E,MAAM,WAAW,wBAAwB;IACxC,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,EAAE,eAAe,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,gBAAgB,IAAI,aAAa,CAAC;IAClC,oEAAoE;IACpE,kBAAkB,IAAI,eAAe,CAAC;IACtC,gBAAgB,IAAI,YAAY,EAAE,CAAC;IACnC,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IACjD,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;IACzG,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACrC,mBAAmB,IAAI,IAAI,CAAC;IAC5B,gBAAgB,IAAI,IAAI,CAAC;IACzB,gFAAgF;IAChF,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,+CAA+C;IAC/C,aAAa,IAAI,IAAI,CAAC;IACtB,iBAAiB,IAAI,OAAO,CAAC;CAC7B;AAED,qBAAa,oBAAoB;IAKpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,0BAA0B,CAA0C;IAC5E,OAAO,CAAC,8BAA8B,CAA0C;IAChF,OAAO,CAAC,0BAA0B,CAAS;IAE3C,YAA6B,IAAI,EAAE,wBAAwB,EAAI;IAE/D,6DAA6D;IAC7D,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,yCAAyC;IACzC,IAAI,qBAAqB,IAAI,OAAO,CAEnC;IAED,sCAAsC;IACtC,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE/C;IAED,4FAA4F;IAC5F,qBAAqB,IAAI,IAAI,CAE5B;IAED,sDAAsD;IACtD,eAAe,IAAI,IAAI,CAGtB;YAWa,gBAAgB;IAuE9B;;;;OAIG;IACG,OAAO,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAkEpE;IAED;;;;;;;;;;OAUG;IACG,eAAe,CAAC,gBAAgB,EAAE,gBAAgB,EAAE,gBAAgB,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA8EhG;YAKa,kBAAkB;CAwGhC","sourcesContent":["/**\n * Compaction controller for AgentSession.\n *\n * Owns manual compaction (the /compact flow) and automatic compaction (overflow\n * recovery and threshold-triggered). It runs the shared apply pipeline — the\n * `session_before_compact` extension hook, summary generation, persistence,\n * agent-context refresh, and the `session_compact` event — and manages the\n * abort controllers and one-shot overflow-recovery guard. Extracted from\n * agent-session.ts behind a narrow CompactionControllerDeps interface.\n */\n\nimport type {\n\tAgentMessage,\n\tCompactionPreparation,\n\tCompactionResult,\n\tThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport {\n\tcalculateContextTokens,\n\tcompact,\n\testimateContextTokens,\n\tprepareCompaction,\n\tshouldCompact,\n} from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport { isContextOverflow } from \"@kolisachint/hoocode-ai\";\nimport type { AgentSessionEvent } from \"./agent-session.js\";\nimport { formatNoModelSelectedMessage } from \"./auth-guidance.js\";\nimport type { ExtensionRunner, SessionBeforeCompactResult } from \"./extensions/index.js\";\nimport type { ModelRegistry } from \"./model-registry.js\";\nimport type { CompactionEntry, SessionEntry, SessionManager } from \"./session-manager.js\";\nimport { getLatestCompactionEntry } from \"./session-manager.js\";\nimport type { SettingsManager } from \"./settings-manager.js\";\n\n/** Narrow dependencies the compaction controller needs from AgentSession. */\nexport interface CompactionControllerDeps {\n\tsessionManager: SessionManager;\n\tsettingsManager: SettingsManager;\n\tmodelRegistry: ModelRegistry;\n\tgetModel(): Model<any> | undefined;\n\tgetThinkingLevel(): ThinkingLevel;\n\t/** Read at call time; the extension runner is swapped on reload. */\n\tgetExtensionRunner(): ExtensionRunner;\n\tgetAgentMessages(): AgentMessage[];\n\tsetAgentMessages(messages: AgentMessage[]): void;\n\tgetRequiredRequestAuth(model: Model<any>): Promise<{ apiKey: string; headers?: Record<string, string> }>;\n\temit(event: AgentSessionEvent): void;\n\tdisconnectFromAgent(): void;\n\treconnectToAgent(): void;\n\t/** Abort the current agent operation and wait for idle (AgentSession.abort). */\n\tabortSession(): Promise<void>;\n\t/** Fire-and-forget continue() on the agent. */\n\tcontinueAgent(): void;\n\thasQueuedMessages(): boolean;\n}\n\nexport class CompactionController {\n\tprivate _compactionAbortController: AbortController | undefined = undefined;\n\tprivate _autoCompactionAbortController: AbortController | undefined = undefined;\n\tprivate _overflowRecoveryAttempted = false;\n\n\tconstructor(private readonly deps: CompactionControllerDeps) {}\n\n\t/** Whether manual or auto compaction is currently running */\n\tget isCompacting(): boolean {\n\t\treturn this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined;\n\t}\n\n\t/** Whether auto-compaction is enabled */\n\tget autoCompactionEnabled(): boolean {\n\t\treturn this.deps.settingsManager.getCompactionEnabled();\n\t}\n\n\t/** Toggle auto-compaction setting. */\n\tsetAutoCompactionEnabled(enabled: boolean): void {\n\t\tthis.deps.settingsManager.setCompactionEnabled(enabled);\n\t}\n\n\t/** Clear the one-shot overflow-recovery guard (on new user input / successful response). */\n\tresetOverflowRecovery(): void {\n\t\tthis._overflowRecoveryAttempted = false;\n\t}\n\n\t/** Cancel in-progress compaction (manual or auto). */\n\tabortCompaction(): void {\n\t\tthis._compactionAbortController?.abort();\n\t\tthis._autoCompactionAbortController?.abort();\n\t}\n\n\t/**\n\t * Shared core for manual and auto compaction.\n\t *\n\t * Runs the `session_before_compact` extension hook, produces the compaction\n\t * (from an extension or by summarizing), persists it, updates agent context,\n\t * and emits `session_compact`. Returns `{ status: \"cancelled\" }` if an\n\t * extension cancels or the signal aborts; callers map that to their own\n\t * cancel handling (manual throws, auto emits).\n\t */\n\tprivate async _applyCompaction(params: {\n\t\tpreparation: CompactionPreparation;\n\t\tbranchEntries: SessionEntry[];\n\t\tmodel: Model<any>;\n\t\tapiKey: string;\n\t\theaders?: Record<string, string>;\n\t\tcustomInstructions?: string;\n\t\tsignal: AbortSignal;\n\t}): Promise<{ status: \"ok\"; result: CompactionResult } | { status: \"cancelled\" }> {\n\t\tconst { preparation, branchEntries, model, apiKey, headers, customInstructions, signal } = params;\n\t\tconst extensionRunner = this.deps.getExtensionRunner();\n\n\t\tlet extensionCompaction: CompactionResult | undefined;\n\t\tlet fromExtension = false;\n\n\t\tif (extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t\tconst result = (await extensionRunner.emit({\n\t\t\t\ttype: \"session_before_compact\",\n\t\t\t\tpreparation,\n\t\t\t\tbranchEntries,\n\t\t\t\tcustomInstructions,\n\t\t\t\tsignal,\n\t\t\t})) as SessionBeforeCompactResult | undefined;\n\n\t\t\tif (result?.cancel) {\n\t\t\t\treturn { status: \"cancelled\" };\n\t\t\t}\n\t\t\tif (result?.compaction) {\n\t\t\t\textensionCompaction = result.compaction;\n\t\t\t\tfromExtension = true;\n\t\t\t}\n\t\t}\n\n\t\tconst generated =\n\t\t\textensionCompaction ??\n\t\t\t(await compact(preparation, model, apiKey, headers, customInstructions, signal, this.deps.getThinkingLevel()));\n\n\t\tif (signal.aborted) {\n\t\t\treturn { status: \"cancelled\" };\n\t\t}\n\n\t\tconst { summary, firstKeptEntryId, tokensBefore, tokensAfter, details } = generated;\n\n\t\tthis.deps.sessionManager.appendCompaction(\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId,\n\t\t\ttokensBefore,\n\t\t\tdetails,\n\t\t\tfromExtension,\n\t\t\ttokensAfter,\n\t\t);\n\t\tconst newEntries = this.deps.sessionManager.getEntries();\n\t\tthis.deps.setAgentMessages(this.deps.sessionManager.buildSessionContext().messages);\n\n\t\tconst savedCompactionEntry = newEntries.find((e) => e.type === \"compaction\" && e.summary === summary) as\n\t\t\t| CompactionEntry\n\t\t\t| undefined;\n\t\tif (savedCompactionEntry) {\n\t\t\tawait extensionRunner.emit({\n\t\t\t\ttype: \"session_compact\",\n\t\t\t\tcompactionEntry: savedCompactionEntry,\n\t\t\t\tfromExtension,\n\t\t\t});\n\t\t}\n\n\t\treturn {\n\t\t\tstatus: \"ok\",\n\t\t\tresult: { summary, firstKeptEntryId, tokensBefore, tokensAfter: tokensAfter ?? tokensBefore, details },\n\t\t};\n\t}\n\n\t/**\n\t * Manually compact the session context.\n\t * Aborts current agent operation first.\n\t * @param customInstructions Optional instructions for the compaction summary\n\t */\n\tasync compact(customInstructions?: string): Promise<CompactionResult> {\n\t\tthis.deps.disconnectFromAgent();\n\t\tawait this.deps.abortSession();\n\t\tthis._compactionAbortController = new AbortController();\n\t\tthis.deps.emit({ type: \"compaction_start\", reason: \"manual\" });\n\n\t\ttry {\n\t\t\tconst model = this.deps.getModel();\n\t\t\tif (!model) {\n\t\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t\t}\n\n\t\t\tconst { apiKey, headers } = await this.deps.getRequiredRequestAuth(model);\n\n\t\t\tconst pathEntries = this.deps.sessionManager.getBranch();\n\t\t\tconst settings = this.deps.settingsManager.getCompactionSettings();\n\n\t\t\tconst preparation = prepareCompaction(pathEntries, settings);\n\t\t\tif (!preparation) {\n\t\t\t\t// Check why we can't compact\n\t\t\t\tconst lastEntry = pathEntries[pathEntries.length - 1];\n\t\t\t\tif (lastEntry?.type === \"compaction\") {\n\t\t\t\t\tthrow new Error(\"Already compacted\");\n\t\t\t\t}\n\t\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t\t}\n\n\t\t\tconst applied = await this._applyCompaction({\n\t\t\t\tpreparation,\n\t\t\t\tbranchEntries: pathEntries,\n\t\t\t\tmodel,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tcustomInstructions,\n\t\t\t\tsignal: this._compactionAbortController.signal,\n\t\t\t});\n\n\t\t\tif (applied.status === \"cancelled\") {\n\t\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t\t}\n\n\t\t\tconst compactionResult = applied.result;\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: compactionResult,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t});\n\t\t\treturn compactionResult;\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: undefined,\n\t\t\t\taborted,\n\t\t\t\twillRetry: false,\n\t\t\t\terrorMessage: aborted ? undefined : `Compaction failed: ${message}`,\n\t\t\t});\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tthis._compactionAbortController = undefined;\n\t\t\tthis.deps.reconnectToAgent();\n\t\t}\n\t}\n\n\t/**\n\t * Check if compaction is needed and run it.\n\t * Called after agent_end and before prompt submission.\n\t *\n\t * Two cases:\n\t * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry\n\t * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)\n\t *\n\t * @param assistantMessage The assistant message to check\n\t * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n\t */\n\tasync checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<void> {\n\t\tconst settings = this.deps.settingsManager.getCompactionSettings();\n\t\tif (!settings.enabled) return;\n\n\t\t// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false\n\t\tif (skipAbortedCheck && assistantMessage.stopReason === \"aborted\") return;\n\n\t\tconst model = this.deps.getModel();\n\t\tconst contextWindow = model?.contextWindow ?? 0;\n\n\t\t// Skip overflow check if the message came from a different model.\n\t\t// This handles the case where user switched from a smaller-context model (e.g. opus)\n\t\t// to a larger-context model (e.g. codex) - the overflow error from the old model\n\t\t// shouldn't trigger compaction for the new model.\n\t\tconst sameModel = model && assistantMessage.provider === model.provider && assistantMessage.model === model.id;\n\n\t\t// Skip compaction checks if this assistant message is older than the latest\n\t\t// compaction boundary. This prevents a stale pre-compaction usage/error\n\t\t// from retriggering compaction on the first prompt after compaction.\n\t\tconst compactionEntry = getLatestCompactionEntry(this.deps.sessionManager.getBranch());\n\t\tconst assistantIsFromBeforeCompaction =\n\t\t\tcompactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();\n\t\tif (assistantIsFromBeforeCompaction) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Case 1: Overflow - LLM returned context overflow error\n\t\tif (sameModel && isContextOverflow(assistantMessage, contextWindow)) {\n\t\t\tif (this._overflowRecoveryAttempted) {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason: \"overflow\",\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage:\n\t\t\t\t\t\t\"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.\",\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis._overflowRecoveryAttempted = true;\n\t\t\t// Remove the error message from agent state (it IS saved to session for history,\n\t\t\t// but we don't want it in context for the retry)\n\t\t\tconst messages = this.deps.getAgentMessages();\n\t\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t\t}\n\t\t\tawait this._runAutoCompaction(\"overflow\", true);\n\t\t\treturn;\n\t\t}\n\n\t\t// Case 2: Threshold - context is getting large\n\t\t// For error messages (no usage data), estimate from last successful response.\n\t\t// This ensures sessions that hit persistent API errors (e.g. 529) can still compact.\n\t\tlet contextTokens: number;\n\t\tif (assistantMessage.stopReason === \"error\") {\n\t\t\tconst messages = this.deps.getAgentMessages();\n\t\t\tconst estimate = estimateContextTokens(messages);\n\t\t\tif (estimate.lastUsageIndex === null) return; // No usage data at all\n\t\t\t// Verify the usage source is post-compaction. Kept pre-compaction messages\n\t\t\t// have stale usage reflecting the old (larger) context and would falsely\n\t\t\t// trigger compaction right after one just finished.\n\t\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\t\tif (\n\t\t\t\tcompactionEntry &&\n\t\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcontextTokens = estimate.tokens;\n\t\t} else {\n\t\t\tcontextTokens = calculateContextTokens(assistantMessage.usage);\n\t\t}\n\t\tif (shouldCompact(contextTokens, contextWindow, settings)) {\n\t\t\tawait this._runAutoCompaction(\"threshold\", false);\n\t\t}\n\t}\n\n\t/**\n\t * Internal: Run auto-compaction with events.\n\t */\n\tprivate async _runAutoCompaction(reason: \"overflow\" | \"threshold\", willRetry: boolean): Promise<void> {\n\t\tconst settings = this.deps.settingsManager.getCompactionSettings();\n\n\t\tthis.deps.emit({ type: \"compaction_start\", reason });\n\t\tthis._autoCompactionAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tconst model = this.deps.getModel();\n\t\t\tif (!model) {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst authResult = await this.deps.modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!authResult.ok || !authResult.apiKey) {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { apiKey, headers } = authResult;\n\n\t\t\tconst pathEntries = this.deps.sessionManager.getBranch();\n\n\t\t\tconst preparation = prepareCompaction(pathEntries, settings);\n\t\t\tif (!preparation) {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst applied = await this._applyCompaction({\n\t\t\t\tpreparation,\n\t\t\t\tbranchEntries: pathEntries,\n\t\t\t\tmodel,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tcustomInstructions: undefined,\n\t\t\t\tsignal: this._autoCompactionAbortController.signal,\n\t\t\t});\n\n\t\t\tif (applied.status === \"cancelled\") {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: true,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst result = applied.result;\n\t\t\tthis.deps.emit({ type: \"compaction_end\", reason, result, aborted: false, willRetry });\n\n\t\t\tif (willRetry) {\n\t\t\t\tconst messages = this.deps.getAgentMessages();\n\t\t\t\tconst lastMsg = messages[messages.length - 1];\n\t\t\t\tif (lastMsg?.role === \"assistant\" && (lastMsg as AssistantMessage).stopReason === \"error\") {\n\t\t\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t\t\t}\n\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tthis.deps.continueAgent();\n\t\t\t\t}, 100);\n\t\t\t} else if (this.deps.hasQueuedMessages()) {\n\t\t\t\t// Auto-compaction can complete while follow-up/steering/custom messages are waiting.\n\t\t\t\t// Kick the loop so queued messages are actually delivered.\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tthis.deps.continueAgent();\n\t\t\t\t}, 100);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"compaction failed\";\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason,\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\terrorMessage:\n\t\t\t\t\treason === \"overflow\"\n\t\t\t\t\t\t? `Context overflow recovery failed: ${errorMessage}`\n\t\t\t\t\t\t: `Auto-compaction failed: ${errorMessage}`,\n\t\t\t});\n\t\t} finally {\n\t\t\tthis._autoCompactionAbortController = undefined;\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Compaction controller for AgentSession.
3
+ *
4
+ * Owns manual compaction (the /compact flow) and automatic compaction (overflow
5
+ * recovery and threshold-triggered). It runs the shared apply pipeline — the
6
+ * `session_before_compact` extension hook, summary generation, persistence,
7
+ * agent-context refresh, and the `session_compact` event — and manages the
8
+ * abort controllers and one-shot overflow-recovery guard. Extracted from
9
+ * agent-session.ts behind a narrow CompactionControllerDeps interface.
10
+ */
11
+ import { calculateContextTokens, compact, estimateContextTokens, prepareCompaction, shouldCompact, } from "@kolisachint/hoocode-agent-core";
12
+ import { isContextOverflow } from "@kolisachint/hoocode-ai";
13
+ import { formatNoModelSelectedMessage } from "./auth-guidance.js";
14
+ import { getLatestCompactionEntry } from "./session-manager.js";
15
+ export class CompactionController {
16
+ deps;
17
+ _compactionAbortController = undefined;
18
+ _autoCompactionAbortController = undefined;
19
+ _overflowRecoveryAttempted = false;
20
+ constructor(deps) {
21
+ this.deps = deps;
22
+ }
23
+ /** Whether manual or auto compaction is currently running */
24
+ get isCompacting() {
25
+ return this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined;
26
+ }
27
+ /** Whether auto-compaction is enabled */
28
+ get autoCompactionEnabled() {
29
+ return this.deps.settingsManager.getCompactionEnabled();
30
+ }
31
+ /** Toggle auto-compaction setting. */
32
+ setAutoCompactionEnabled(enabled) {
33
+ this.deps.settingsManager.setCompactionEnabled(enabled);
34
+ }
35
+ /** Clear the one-shot overflow-recovery guard (on new user input / successful response). */
36
+ resetOverflowRecovery() {
37
+ this._overflowRecoveryAttempted = false;
38
+ }
39
+ /** Cancel in-progress compaction (manual or auto). */
40
+ abortCompaction() {
41
+ this._compactionAbortController?.abort();
42
+ this._autoCompactionAbortController?.abort();
43
+ }
44
+ /**
45
+ * Shared core for manual and auto compaction.
46
+ *
47
+ * Runs the `session_before_compact` extension hook, produces the compaction
48
+ * (from an extension or by summarizing), persists it, updates agent context,
49
+ * and emits `session_compact`. Returns `{ status: "cancelled" }` if an
50
+ * extension cancels or the signal aborts; callers map that to their own
51
+ * cancel handling (manual throws, auto emits).
52
+ */
53
+ async _applyCompaction(params) {
54
+ const { preparation, branchEntries, model, apiKey, headers, customInstructions, signal } = params;
55
+ const extensionRunner = this.deps.getExtensionRunner();
56
+ let extensionCompaction;
57
+ let fromExtension = false;
58
+ if (extensionRunner.hasHandlers("session_before_compact")) {
59
+ const result = (await extensionRunner.emit({
60
+ type: "session_before_compact",
61
+ preparation,
62
+ branchEntries,
63
+ customInstructions,
64
+ signal,
65
+ }));
66
+ if (result?.cancel) {
67
+ return { status: "cancelled" };
68
+ }
69
+ if (result?.compaction) {
70
+ extensionCompaction = result.compaction;
71
+ fromExtension = true;
72
+ }
73
+ }
74
+ const generated = extensionCompaction ??
75
+ (await compact(preparation, model, apiKey, headers, customInstructions, signal, this.deps.getThinkingLevel()));
76
+ if (signal.aborted) {
77
+ return { status: "cancelled" };
78
+ }
79
+ const { summary, firstKeptEntryId, tokensBefore, tokensAfter, details } = generated;
80
+ this.deps.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, tokensAfter);
81
+ const newEntries = this.deps.sessionManager.getEntries();
82
+ this.deps.setAgentMessages(this.deps.sessionManager.buildSessionContext().messages);
83
+ const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
84
+ if (savedCompactionEntry) {
85
+ await extensionRunner.emit({
86
+ type: "session_compact",
87
+ compactionEntry: savedCompactionEntry,
88
+ fromExtension,
89
+ });
90
+ }
91
+ return {
92
+ status: "ok",
93
+ result: { summary, firstKeptEntryId, tokensBefore, tokensAfter: tokensAfter ?? tokensBefore, details },
94
+ };
95
+ }
96
+ /**
97
+ * Manually compact the session context.
98
+ * Aborts current agent operation first.
99
+ * @param customInstructions Optional instructions for the compaction summary
100
+ */
101
+ async compact(customInstructions) {
102
+ this.deps.disconnectFromAgent();
103
+ await this.deps.abortSession();
104
+ this._compactionAbortController = new AbortController();
105
+ this.deps.emit({ type: "compaction_start", reason: "manual" });
106
+ try {
107
+ const model = this.deps.getModel();
108
+ if (!model) {
109
+ throw new Error(formatNoModelSelectedMessage());
110
+ }
111
+ const { apiKey, headers } = await this.deps.getRequiredRequestAuth(model);
112
+ const pathEntries = this.deps.sessionManager.getBranch();
113
+ const settings = this.deps.settingsManager.getCompactionSettings();
114
+ const preparation = prepareCompaction(pathEntries, settings);
115
+ if (!preparation) {
116
+ // Check why we can't compact
117
+ const lastEntry = pathEntries[pathEntries.length - 1];
118
+ if (lastEntry?.type === "compaction") {
119
+ throw new Error("Already compacted");
120
+ }
121
+ throw new Error("Nothing to compact (session too small)");
122
+ }
123
+ const applied = await this._applyCompaction({
124
+ preparation,
125
+ branchEntries: pathEntries,
126
+ model,
127
+ apiKey,
128
+ headers,
129
+ customInstructions,
130
+ signal: this._compactionAbortController.signal,
131
+ });
132
+ if (applied.status === "cancelled") {
133
+ throw new Error("Compaction cancelled");
134
+ }
135
+ const compactionResult = applied.result;
136
+ this.deps.emit({
137
+ type: "compaction_end",
138
+ reason: "manual",
139
+ result: compactionResult,
140
+ aborted: false,
141
+ willRetry: false,
142
+ });
143
+ return compactionResult;
144
+ }
145
+ catch (error) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
148
+ this.deps.emit({
149
+ type: "compaction_end",
150
+ reason: "manual",
151
+ result: undefined,
152
+ aborted,
153
+ willRetry: false,
154
+ errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
155
+ });
156
+ throw error;
157
+ }
158
+ finally {
159
+ this._compactionAbortController = undefined;
160
+ this.deps.reconnectToAgent();
161
+ }
162
+ }
163
+ /**
164
+ * Check if compaction is needed and run it.
165
+ * Called after agent_end and before prompt submission.
166
+ *
167
+ * Two cases:
168
+ * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry
169
+ * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)
170
+ *
171
+ * @param assistantMessage The assistant message to check
172
+ * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
173
+ */
174
+ async checkCompaction(assistantMessage, skipAbortedCheck = true) {
175
+ const settings = this.deps.settingsManager.getCompactionSettings();
176
+ if (!settings.enabled)
177
+ return;
178
+ // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
179
+ if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
180
+ return;
181
+ const model = this.deps.getModel();
182
+ const contextWindow = model?.contextWindow ?? 0;
183
+ // Skip overflow check if the message came from a different model.
184
+ // This handles the case where user switched from a smaller-context model (e.g. opus)
185
+ // to a larger-context model (e.g. codex) - the overflow error from the old model
186
+ // shouldn't trigger compaction for the new model.
187
+ const sameModel = model && assistantMessage.provider === model.provider && assistantMessage.model === model.id;
188
+ // Skip compaction checks if this assistant message is older than the latest
189
+ // compaction boundary. This prevents a stale pre-compaction usage/error
190
+ // from retriggering compaction on the first prompt after compaction.
191
+ const compactionEntry = getLatestCompactionEntry(this.deps.sessionManager.getBranch());
192
+ const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
193
+ if (assistantIsFromBeforeCompaction) {
194
+ return;
195
+ }
196
+ // Case 1: Overflow - LLM returned context overflow error
197
+ if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
198
+ if (this._overflowRecoveryAttempted) {
199
+ this.deps.emit({
200
+ type: "compaction_end",
201
+ reason: "overflow",
202
+ result: undefined,
203
+ aborted: false,
204
+ willRetry: false,
205
+ errorMessage: "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
206
+ });
207
+ return;
208
+ }
209
+ this._overflowRecoveryAttempted = true;
210
+ // Remove the error message from agent state (it IS saved to session for history,
211
+ // but we don't want it in context for the retry)
212
+ const messages = this.deps.getAgentMessages();
213
+ if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
214
+ this.deps.setAgentMessages(messages.slice(0, -1));
215
+ }
216
+ await this._runAutoCompaction("overflow", true);
217
+ return;
218
+ }
219
+ // Case 2: Threshold - context is getting large
220
+ // For error messages (no usage data), estimate from last successful response.
221
+ // This ensures sessions that hit persistent API errors (e.g. 529) can still compact.
222
+ let contextTokens;
223
+ if (assistantMessage.stopReason === "error") {
224
+ const messages = this.deps.getAgentMessages();
225
+ const estimate = estimateContextTokens(messages);
226
+ if (estimate.lastUsageIndex === null)
227
+ return; // No usage data at all
228
+ // Verify the usage source is post-compaction. Kept pre-compaction messages
229
+ // have stale usage reflecting the old (larger) context and would falsely
230
+ // trigger compaction right after one just finished.
231
+ const usageMsg = messages[estimate.lastUsageIndex];
232
+ if (compactionEntry &&
233
+ usageMsg.role === "assistant" &&
234
+ usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
235
+ return;
236
+ }
237
+ contextTokens = estimate.tokens;
238
+ }
239
+ else {
240
+ contextTokens = calculateContextTokens(assistantMessage.usage);
241
+ }
242
+ if (shouldCompact(contextTokens, contextWindow, settings)) {
243
+ await this._runAutoCompaction("threshold", false);
244
+ }
245
+ }
246
+ /**
247
+ * Internal: Run auto-compaction with events.
248
+ */
249
+ async _runAutoCompaction(reason, willRetry) {
250
+ const settings = this.deps.settingsManager.getCompactionSettings();
251
+ this.deps.emit({ type: "compaction_start", reason });
252
+ this._autoCompactionAbortController = new AbortController();
253
+ try {
254
+ const model = this.deps.getModel();
255
+ if (!model) {
256
+ this.deps.emit({
257
+ type: "compaction_end",
258
+ reason,
259
+ result: undefined,
260
+ aborted: false,
261
+ willRetry: false,
262
+ });
263
+ return;
264
+ }
265
+ const authResult = await this.deps.modelRegistry.getApiKeyAndHeaders(model);
266
+ if (!authResult.ok || !authResult.apiKey) {
267
+ this.deps.emit({
268
+ type: "compaction_end",
269
+ reason,
270
+ result: undefined,
271
+ aborted: false,
272
+ willRetry: false,
273
+ });
274
+ return;
275
+ }
276
+ const { apiKey, headers } = authResult;
277
+ const pathEntries = this.deps.sessionManager.getBranch();
278
+ const preparation = prepareCompaction(pathEntries, settings);
279
+ if (!preparation) {
280
+ this.deps.emit({
281
+ type: "compaction_end",
282
+ reason,
283
+ result: undefined,
284
+ aborted: false,
285
+ willRetry: false,
286
+ });
287
+ return;
288
+ }
289
+ const applied = await this._applyCompaction({
290
+ preparation,
291
+ branchEntries: pathEntries,
292
+ model,
293
+ apiKey,
294
+ headers,
295
+ customInstructions: undefined,
296
+ signal: this._autoCompactionAbortController.signal,
297
+ });
298
+ if (applied.status === "cancelled") {
299
+ this.deps.emit({
300
+ type: "compaction_end",
301
+ reason,
302
+ result: undefined,
303
+ aborted: true,
304
+ willRetry: false,
305
+ });
306
+ return;
307
+ }
308
+ const result = applied.result;
309
+ this.deps.emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
310
+ if (willRetry) {
311
+ const messages = this.deps.getAgentMessages();
312
+ const lastMsg = messages[messages.length - 1];
313
+ if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") {
314
+ this.deps.setAgentMessages(messages.slice(0, -1));
315
+ }
316
+ setTimeout(() => {
317
+ this.deps.continueAgent();
318
+ }, 100);
319
+ }
320
+ else if (this.deps.hasQueuedMessages()) {
321
+ // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
322
+ // Kick the loop so queued messages are actually delivered.
323
+ setTimeout(() => {
324
+ this.deps.continueAgent();
325
+ }, 100);
326
+ }
327
+ }
328
+ catch (error) {
329
+ const errorMessage = error instanceof Error ? error.message : "compaction failed";
330
+ this.deps.emit({
331
+ type: "compaction_end",
332
+ reason,
333
+ result: undefined,
334
+ aborted: false,
335
+ willRetry: false,
336
+ errorMessage: reason === "overflow"
337
+ ? `Context overflow recovery failed: ${errorMessage}`
338
+ : `Auto-compaction failed: ${errorMessage}`,
339
+ });
340
+ }
341
+ finally {
342
+ this._autoCompactionAbortController = undefined;
343
+ }
344
+ }
345
+ }
346
+ //# sourceMappingURL=agent-session-compaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-compaction.js","sourceRoot":"","sources":["../../src/core/agent-session-compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Compaction controller for AgentSession.\n *\n * Owns manual compaction (the /compact flow) and automatic compaction (overflow\n * recovery and threshold-triggered). It runs the shared apply pipeline — the\n * `session_before_compact` extension hook, summary generation, persistence,\n * agent-context refresh, and the `session_compact` event — and manages the\n * abort controllers and one-shot overflow-recovery guard. Extracted from\n * agent-session.ts behind a narrow CompactionControllerDeps interface.\n */\n\nimport type {\n\tAgentMessage,\n\tCompactionPreparation,\n\tCompactionResult,\n\tThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport {\n\tcalculateContextTokens,\n\tcompact,\n\testimateContextTokens,\n\tprepareCompaction,\n\tshouldCompact,\n} from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport { isContextOverflow } from \"@kolisachint/hoocode-ai\";\nimport type { AgentSessionEvent } from \"./agent-session.js\";\nimport { formatNoModelSelectedMessage } from \"./auth-guidance.js\";\nimport type { ExtensionRunner, SessionBeforeCompactResult } from \"./extensions/index.js\";\nimport type { ModelRegistry } from \"./model-registry.js\";\nimport type { CompactionEntry, SessionEntry, SessionManager } from \"./session-manager.js\";\nimport { getLatestCompactionEntry } from \"./session-manager.js\";\nimport type { SettingsManager } from \"./settings-manager.js\";\n\n/** Narrow dependencies the compaction controller needs from AgentSession. */\nexport interface CompactionControllerDeps {\n\tsessionManager: SessionManager;\n\tsettingsManager: SettingsManager;\n\tmodelRegistry: ModelRegistry;\n\tgetModel(): Model<any> | undefined;\n\tgetThinkingLevel(): ThinkingLevel;\n\t/** Read at call time; the extension runner is swapped on reload. */\n\tgetExtensionRunner(): ExtensionRunner;\n\tgetAgentMessages(): AgentMessage[];\n\tsetAgentMessages(messages: AgentMessage[]): void;\n\tgetRequiredRequestAuth(model: Model<any>): Promise<{ apiKey: string; headers?: Record<string, string> }>;\n\temit(event: AgentSessionEvent): void;\n\tdisconnectFromAgent(): void;\n\treconnectToAgent(): void;\n\t/** Abort the current agent operation and wait for idle (AgentSession.abort). */\n\tabortSession(): Promise<void>;\n\t/** Fire-and-forget continue() on the agent. */\n\tcontinueAgent(): void;\n\thasQueuedMessages(): boolean;\n}\n\nexport class CompactionController {\n\tprivate _compactionAbortController: AbortController | undefined = undefined;\n\tprivate _autoCompactionAbortController: AbortController | undefined = undefined;\n\tprivate _overflowRecoveryAttempted = false;\n\n\tconstructor(private readonly deps: CompactionControllerDeps) {}\n\n\t/** Whether manual or auto compaction is currently running */\n\tget isCompacting(): boolean {\n\t\treturn this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined;\n\t}\n\n\t/** Whether auto-compaction is enabled */\n\tget autoCompactionEnabled(): boolean {\n\t\treturn this.deps.settingsManager.getCompactionEnabled();\n\t}\n\n\t/** Toggle auto-compaction setting. */\n\tsetAutoCompactionEnabled(enabled: boolean): void {\n\t\tthis.deps.settingsManager.setCompactionEnabled(enabled);\n\t}\n\n\t/** Clear the one-shot overflow-recovery guard (on new user input / successful response). */\n\tresetOverflowRecovery(): void {\n\t\tthis._overflowRecoveryAttempted = false;\n\t}\n\n\t/** Cancel in-progress compaction (manual or auto). */\n\tabortCompaction(): void {\n\t\tthis._compactionAbortController?.abort();\n\t\tthis._autoCompactionAbortController?.abort();\n\t}\n\n\t/**\n\t * Shared core for manual and auto compaction.\n\t *\n\t * Runs the `session_before_compact` extension hook, produces the compaction\n\t * (from an extension or by summarizing), persists it, updates agent context,\n\t * and emits `session_compact`. Returns `{ status: \"cancelled\" }` if an\n\t * extension cancels or the signal aborts; callers map that to their own\n\t * cancel handling (manual throws, auto emits).\n\t */\n\tprivate async _applyCompaction(params: {\n\t\tpreparation: CompactionPreparation;\n\t\tbranchEntries: SessionEntry[];\n\t\tmodel: Model<any>;\n\t\tapiKey: string;\n\t\theaders?: Record<string, string>;\n\t\tcustomInstructions?: string;\n\t\tsignal: AbortSignal;\n\t}): Promise<{ status: \"ok\"; result: CompactionResult } | { status: \"cancelled\" }> {\n\t\tconst { preparation, branchEntries, model, apiKey, headers, customInstructions, signal } = params;\n\t\tconst extensionRunner = this.deps.getExtensionRunner();\n\n\t\tlet extensionCompaction: CompactionResult | undefined;\n\t\tlet fromExtension = false;\n\n\t\tif (extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t\tconst result = (await extensionRunner.emit({\n\t\t\t\ttype: \"session_before_compact\",\n\t\t\t\tpreparation,\n\t\t\t\tbranchEntries,\n\t\t\t\tcustomInstructions,\n\t\t\t\tsignal,\n\t\t\t})) as SessionBeforeCompactResult | undefined;\n\n\t\t\tif (result?.cancel) {\n\t\t\t\treturn { status: \"cancelled\" };\n\t\t\t}\n\t\t\tif (result?.compaction) {\n\t\t\t\textensionCompaction = result.compaction;\n\t\t\t\tfromExtension = true;\n\t\t\t}\n\t\t}\n\n\t\tconst generated =\n\t\t\textensionCompaction ??\n\t\t\t(await compact(preparation, model, apiKey, headers, customInstructions, signal, this.deps.getThinkingLevel()));\n\n\t\tif (signal.aborted) {\n\t\t\treturn { status: \"cancelled\" };\n\t\t}\n\n\t\tconst { summary, firstKeptEntryId, tokensBefore, tokensAfter, details } = generated;\n\n\t\tthis.deps.sessionManager.appendCompaction(\n\t\t\tsummary,\n\t\t\tfirstKeptEntryId,\n\t\t\ttokensBefore,\n\t\t\tdetails,\n\t\t\tfromExtension,\n\t\t\ttokensAfter,\n\t\t);\n\t\tconst newEntries = this.deps.sessionManager.getEntries();\n\t\tthis.deps.setAgentMessages(this.deps.sessionManager.buildSessionContext().messages);\n\n\t\tconst savedCompactionEntry = newEntries.find((e) => e.type === \"compaction\" && e.summary === summary) as\n\t\t\t| CompactionEntry\n\t\t\t| undefined;\n\t\tif (savedCompactionEntry) {\n\t\t\tawait extensionRunner.emit({\n\t\t\t\ttype: \"session_compact\",\n\t\t\t\tcompactionEntry: savedCompactionEntry,\n\t\t\t\tfromExtension,\n\t\t\t});\n\t\t}\n\n\t\treturn {\n\t\t\tstatus: \"ok\",\n\t\t\tresult: { summary, firstKeptEntryId, tokensBefore, tokensAfter: tokensAfter ?? tokensBefore, details },\n\t\t};\n\t}\n\n\t/**\n\t * Manually compact the session context.\n\t * Aborts current agent operation first.\n\t * @param customInstructions Optional instructions for the compaction summary\n\t */\n\tasync compact(customInstructions?: string): Promise<CompactionResult> {\n\t\tthis.deps.disconnectFromAgent();\n\t\tawait this.deps.abortSession();\n\t\tthis._compactionAbortController = new AbortController();\n\t\tthis.deps.emit({ type: \"compaction_start\", reason: \"manual\" });\n\n\t\ttry {\n\t\t\tconst model = this.deps.getModel();\n\t\t\tif (!model) {\n\t\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t\t}\n\n\t\t\tconst { apiKey, headers } = await this.deps.getRequiredRequestAuth(model);\n\n\t\t\tconst pathEntries = this.deps.sessionManager.getBranch();\n\t\t\tconst settings = this.deps.settingsManager.getCompactionSettings();\n\n\t\t\tconst preparation = prepareCompaction(pathEntries, settings);\n\t\t\tif (!preparation) {\n\t\t\t\t// Check why we can't compact\n\t\t\t\tconst lastEntry = pathEntries[pathEntries.length - 1];\n\t\t\t\tif (lastEntry?.type === \"compaction\") {\n\t\t\t\t\tthrow new Error(\"Already compacted\");\n\t\t\t\t}\n\t\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t\t}\n\n\t\t\tconst applied = await this._applyCompaction({\n\t\t\t\tpreparation,\n\t\t\t\tbranchEntries: pathEntries,\n\t\t\t\tmodel,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tcustomInstructions,\n\t\t\t\tsignal: this._compactionAbortController.signal,\n\t\t\t});\n\n\t\t\tif (applied.status === \"cancelled\") {\n\t\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t\t}\n\n\t\t\tconst compactionResult = applied.result;\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: compactionResult,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t});\n\t\t\treturn compactionResult;\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: undefined,\n\t\t\t\taborted,\n\t\t\t\twillRetry: false,\n\t\t\t\terrorMessage: aborted ? undefined : `Compaction failed: ${message}`,\n\t\t\t});\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tthis._compactionAbortController = undefined;\n\t\t\tthis.deps.reconnectToAgent();\n\t\t}\n\t}\n\n\t/**\n\t * Check if compaction is needed and run it.\n\t * Called after agent_end and before prompt submission.\n\t *\n\t * Two cases:\n\t * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry\n\t * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)\n\t *\n\t * @param assistantMessage The assistant message to check\n\t * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n\t */\n\tasync checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<void> {\n\t\tconst settings = this.deps.settingsManager.getCompactionSettings();\n\t\tif (!settings.enabled) return;\n\n\t\t// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false\n\t\tif (skipAbortedCheck && assistantMessage.stopReason === \"aborted\") return;\n\n\t\tconst model = this.deps.getModel();\n\t\tconst contextWindow = model?.contextWindow ?? 0;\n\n\t\t// Skip overflow check if the message came from a different model.\n\t\t// This handles the case where user switched from a smaller-context model (e.g. opus)\n\t\t// to a larger-context model (e.g. codex) - the overflow error from the old model\n\t\t// shouldn't trigger compaction for the new model.\n\t\tconst sameModel = model && assistantMessage.provider === model.provider && assistantMessage.model === model.id;\n\n\t\t// Skip compaction checks if this assistant message is older than the latest\n\t\t// compaction boundary. This prevents a stale pre-compaction usage/error\n\t\t// from retriggering compaction on the first prompt after compaction.\n\t\tconst compactionEntry = getLatestCompactionEntry(this.deps.sessionManager.getBranch());\n\t\tconst assistantIsFromBeforeCompaction =\n\t\t\tcompactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();\n\t\tif (assistantIsFromBeforeCompaction) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Case 1: Overflow - LLM returned context overflow error\n\t\tif (sameModel && isContextOverflow(assistantMessage, contextWindow)) {\n\t\t\tif (this._overflowRecoveryAttempted) {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason: \"overflow\",\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage:\n\t\t\t\t\t\t\"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.\",\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis._overflowRecoveryAttempted = true;\n\t\t\t// Remove the error message from agent state (it IS saved to session for history,\n\t\t\t// but we don't want it in context for the retry)\n\t\t\tconst messages = this.deps.getAgentMessages();\n\t\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t\t}\n\t\t\tawait this._runAutoCompaction(\"overflow\", true);\n\t\t\treturn;\n\t\t}\n\n\t\t// Case 2: Threshold - context is getting large\n\t\t// For error messages (no usage data), estimate from last successful response.\n\t\t// This ensures sessions that hit persistent API errors (e.g. 529) can still compact.\n\t\tlet contextTokens: number;\n\t\tif (assistantMessage.stopReason === \"error\") {\n\t\t\tconst messages = this.deps.getAgentMessages();\n\t\t\tconst estimate = estimateContextTokens(messages);\n\t\t\tif (estimate.lastUsageIndex === null) return; // No usage data at all\n\t\t\t// Verify the usage source is post-compaction. Kept pre-compaction messages\n\t\t\t// have stale usage reflecting the old (larger) context and would falsely\n\t\t\t// trigger compaction right after one just finished.\n\t\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\t\tif (\n\t\t\t\tcompactionEntry &&\n\t\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcontextTokens = estimate.tokens;\n\t\t} else {\n\t\t\tcontextTokens = calculateContextTokens(assistantMessage.usage);\n\t\t}\n\t\tif (shouldCompact(contextTokens, contextWindow, settings)) {\n\t\t\tawait this._runAutoCompaction(\"threshold\", false);\n\t\t}\n\t}\n\n\t/**\n\t * Internal: Run auto-compaction with events.\n\t */\n\tprivate async _runAutoCompaction(reason: \"overflow\" | \"threshold\", willRetry: boolean): Promise<void> {\n\t\tconst settings = this.deps.settingsManager.getCompactionSettings();\n\n\t\tthis.deps.emit({ type: \"compaction_start\", reason });\n\t\tthis._autoCompactionAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tconst model = this.deps.getModel();\n\t\t\tif (!model) {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst authResult = await this.deps.modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!authResult.ok || !authResult.apiKey) {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { apiKey, headers } = authResult;\n\n\t\t\tconst pathEntries = this.deps.sessionManager.getBranch();\n\n\t\t\tconst preparation = prepareCompaction(pathEntries, settings);\n\t\t\tif (!preparation) {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst applied = await this._applyCompaction({\n\t\t\t\tpreparation,\n\t\t\t\tbranchEntries: pathEntries,\n\t\t\t\tmodel,\n\t\t\t\tapiKey,\n\t\t\t\theaders,\n\t\t\t\tcustomInstructions: undefined,\n\t\t\t\tsignal: this._autoCompactionAbortController.signal,\n\t\t\t});\n\n\t\t\tif (applied.status === \"cancelled\") {\n\t\t\t\tthis.deps.emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: true,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst result = applied.result;\n\t\t\tthis.deps.emit({ type: \"compaction_end\", reason, result, aborted: false, willRetry });\n\n\t\t\tif (willRetry) {\n\t\t\t\tconst messages = this.deps.getAgentMessages();\n\t\t\t\tconst lastMsg = messages[messages.length - 1];\n\t\t\t\tif (lastMsg?.role === \"assistant\" && (lastMsg as AssistantMessage).stopReason === \"error\") {\n\t\t\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t\t\t}\n\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tthis.deps.continueAgent();\n\t\t\t\t}, 100);\n\t\t\t} else if (this.deps.hasQueuedMessages()) {\n\t\t\t\t// Auto-compaction can complete while follow-up/steering/custom messages are waiting.\n\t\t\t\t// Kick the loop so queued messages are actually delivered.\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tthis.deps.continueAgent();\n\t\t\t\t}, 100);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"compaction failed\";\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason,\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\terrorMessage:\n\t\t\t\t\treason === \"overflow\"\n\t\t\t\t\t\t? `Context overflow recovery failed: ${errorMessage}`\n\t\t\t\t\t\t: `Auto-compaction failed: ${errorMessage}`,\n\t\t\t});\n\t\t} finally {\n\t\t\tthis._autoCompactionAbortController = undefined;\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Auto-retry controller for AgentSession.
3
+ *
4
+ * Owns the retry lifecycle for transient assistant errors (overloaded, rate
5
+ * limit, server/network/transport failures): deciding whether an error is
6
+ * retryable, arming the retry promise synchronously on agent_end, backing off
7
+ * exponentially, and re-driving the agent via continue(). Context-overflow
8
+ * errors are intentionally excluded here — those are handled by compaction.
9
+ */
10
+ import type { AgentEvent, AgentMessage } from "@kolisachint/hoocode-agent-core";
11
+ import type { AssistantMessage, Model } from "@kolisachint/hoocode-ai";
12
+ import type { AgentSessionEvent } from "./agent-session.js";
13
+ /** Narrow dependencies the retry controller needs from AgentSession. */
14
+ export interface AutoRetryDeps {
15
+ getRetrySettings(): {
16
+ enabled: boolean;
17
+ maxRetries: number;
18
+ baseDelayMs: number;
19
+ };
20
+ getModel(): Model<any> | undefined;
21
+ getAgentMessages(): AgentMessage[];
22
+ setAgentMessages(messages: AgentMessage[]): void;
23
+ /** Fire-and-forget continue() on the agent (errors surface on the next agent_end). */
24
+ continueAgent(): void;
25
+ waitForAgentIdle(): Promise<void>;
26
+ emit(event: AgentSessionEvent): void;
27
+ }
28
+ export declare class AutoRetryController {
29
+ private readonly deps;
30
+ private _abortController;
31
+ private _attempt;
32
+ private _promise;
33
+ private _resolve;
34
+ constructor(deps: AutoRetryDeps);
35
+ /** Current retry attempt (0 if not retrying) */
36
+ get attempt(): number;
37
+ /** Whether a retry is currently in progress */
38
+ get isRetrying(): boolean;
39
+ /**
40
+ * Check if an error is retryable (overloaded, rate limit, server errors).
41
+ * Context overflow errors are NOT retryable (handled by compaction instead).
42
+ */
43
+ isRetryableError(message: AssistantMessage): boolean;
44
+ /**
45
+ * Create the retry promise synchronously when an agent_end carries a
46
+ * retryable error. Agent.emit() runs handlers synchronously and prompt()
47
+ * calls waitForRetry() as soon as agent.prompt() resolves; arming the promise
48
+ * here (rather than inside async event processing) ensures waitForRetry()
49
+ * never misses an in-flight retry.
50
+ */
51
+ createPromiseForAgentEnd(event: AgentEvent): void;
52
+ private _findLastAssistantInMessages;
53
+ /**
54
+ * Reset the attempt counter after a successful assistant response.
55
+ * Callers invoke this only for non-error responses; it emits a success event
56
+ * when a retry was in progress.
57
+ */
58
+ onSuccessfulAssistantResponse(): void;
59
+ /** Resolve the pending retry promise */
60
+ resolve(): void;
61
+ /**
62
+ * Handle retryable errors with exponential backoff.
63
+ * @returns true if retry was initiated, false if max retries exceeded or disabled
64
+ */
65
+ handleRetryableError(message: AssistantMessage): Promise<boolean>;
66
+ /**
67
+ * Cancel in-progress retry.
68
+ */
69
+ abort(): void;
70
+ /**
71
+ * Wait for any in-progress retry to complete.
72
+ * Returns immediately if no retry is in progress.
73
+ */
74
+ waitForRetry(): Promise<void>;
75
+ }
76
+ //# sourceMappingURL=agent-session-retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session-retry.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAGvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAU5D,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC7B,gBAAgB,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAClF,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,gBAAgB,IAAI,YAAY,EAAE,CAAC;IACnC,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IACjD,sFAAsF;IACtF,aAAa,IAAI,IAAI,CAAC;IACtB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACrC;AAED,qBAAa,mBAAmB;IAMnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,gBAAgB,CAA0C;IAClE,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,QAAQ,CAAuC;IAEvD,YAA6B,IAAI,EAAE,aAAa,EAAI;IAEpD,gDAAgD;IAChD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,+CAA+C;IAC/C,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAQnD;IAED;;;;;;OAMG;IACH,wBAAwB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAkBhD;IAED,OAAO,CAAC,4BAA4B;IAUpC;;;;OAIG;IACH,6BAA6B,IAAI,IAAI,CASpC;IAED,wCAAwC;IACxC,OAAO,IAAI,IAAI,CAMd;IAED;;;OAGG;IACG,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwEtE;IAED;;OAEG;IACH,KAAK,IAAI,IAAI,CAIZ;IAED;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAOlC;CACD","sourcesContent":["/**\n * Auto-retry controller for AgentSession.\n *\n * Owns the retry lifecycle for transient assistant errors (overloaded, rate\n * limit, server/network/transport failures): deciding whether an error is\n * retryable, arming the retry promise synchronously on agent_end, backing off\n * exponentially, and re-driving the agent via continue(). Context-overflow\n * errors are intentionally excluded here — those are handled by compaction.\n */\n\nimport type { AgentEvent, AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport { isContextOverflow } from \"@kolisachint/hoocode-ai\";\nimport { sleep } from \"../utils/sleep.js\";\nimport type { AgentSessionEvent } from \"./agent-session.js\";\n\n/**\n * Retryable error signatures (overloaded, rate limit, server/network errors,\n * transport closes). Compiled once at module load instead of on every assistant\n * response. Context-overflow errors are handled separately by compaction.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;\n\n/** Narrow dependencies the retry controller needs from AgentSession. */\nexport interface AutoRetryDeps {\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number };\n\tgetModel(): Model<any> | undefined;\n\tgetAgentMessages(): AgentMessage[];\n\tsetAgentMessages(messages: AgentMessage[]): void;\n\t/** Fire-and-forget continue() on the agent (errors surface on the next agent_end). */\n\tcontinueAgent(): void;\n\twaitForAgentIdle(): Promise<void>;\n\temit(event: AgentSessionEvent): void;\n}\n\nexport class AutoRetryController {\n\tprivate _abortController: AbortController | undefined = undefined;\n\tprivate _attempt = 0;\n\tprivate _promise: Promise<void> | undefined = undefined;\n\tprivate _resolve: (() => void) | undefined = undefined;\n\n\tconstructor(private readonly deps: AutoRetryDeps) {}\n\n\t/** Current retry attempt (0 if not retrying) */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** Whether a retry is currently in progress */\n\tget isRetrying(): boolean {\n\t\treturn this._promise !== undefined;\n\t}\n\n\t/**\n\t * Check if an error is retryable (overloaded, rate limit, server errors).\n\t * Context overflow errors are NOT retryable (handled by compaction instead).\n\t */\n\tisRetryableError(message: AssistantMessage): boolean {\n\t\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\n\t\t// Context overflow is handled by compaction, not retry\n\t\tconst contextWindow = this.deps.getModel()?.contextWindow ?? 0;\n\t\tif (isContextOverflow(message, contextWindow)) return false;\n\n\t\treturn RETRYABLE_ERROR_PATTERN.test(message.errorMessage);\n\t}\n\n\t/**\n\t * Create the retry promise synchronously when an agent_end carries a\n\t * retryable error. Agent.emit() runs handlers synchronously and prompt()\n\t * calls waitForRetry() as soon as agent.prompt() resolves; arming the promise\n\t * here (rather than inside async event processing) ensures waitForRetry()\n\t * never misses an in-flight retry.\n\t */\n\tcreatePromiseForAgentEnd(event: AgentEvent): void {\n\t\tif (event.type !== \"agent_end\" || this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst lastAssistant = this._findLastAssistantInMessages(event.messages);\n\t\tif (!lastAssistant || !this.isRetryableError(lastAssistant)) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis._promise = new Promise((resolve) => {\n\t\t\tthis._resolve = resolve;\n\t\t});\n\t}\n\n\tprivate _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined {\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = messages[i];\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\treturn message as AssistantMessage;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Reset the attempt counter after a successful assistant response.\n\t * Callers invoke this only for non-error responses; it emits a success event\n\t * when a retry was in progress.\n\t */\n\tonSuccessfulAssistantResponse(): void {\n\t\tif (this._attempt > 0) {\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: true,\n\t\t\t\tattempt: this._attempt,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t}\n\t}\n\n\t/** Resolve the pending retry promise */\n\tresolve(): void {\n\t\tif (this._resolve) {\n\t\t\tthis._resolve();\n\t\t\tthis._resolve = undefined;\n\t\t\tthis._promise = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Handle retryable errors with exponential backoff.\n\t * @returns true if retry was initiated, false if max retries exceeded or disabled\n\t */\n\tasync handleRetryableError(message: AssistantMessage): Promise<boolean> {\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\n\t\t// Retry promise is created synchronously in createPromiseForAgentEnd for agent_end.\n\t\t// Keep a defensive fallback here in case a future refactor bypasses that path.\n\t\tif (!this._promise) {\n\t\t\tthis._promise = new Promise((resolve) => {\n\t\t\t\tthis._resolve = resolve;\n\t\t\t});\n\t\t}\n\n\t\tthis._attempt++;\n\n\t\tif (this._attempt > settings.maxRetries) {\n\t\t\t// Max retries exceeded, emit final failure and reset\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt: this._attempt - 1,\n\t\t\t\tfinalError: message.errorMessage,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t\tthis.resolve(); // Resolve so waitForRetry() completes\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = settings.baseDelayMs * 2 ** (this._attempt - 1);\n\n\t\tthis.deps.emit({\n\t\t\ttype: \"auto_retry_start\",\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: settings.maxRetries,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove error message from agent state (keep in session for history)\n\t\tconst messages = this.deps.getAgentMessages();\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t}\n\n\t\t// Wait with exponential backoff (abortable)\n\t\tthis._abortController = new AbortController();\n\t\ttry {\n\t\t\tawait sleep(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted during sleep - emit end event so UI can clean up\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis._abortController = undefined;\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt,\n\t\t\t\tfinalError: \"Retry cancelled\",\n\t\t\t});\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\t\tthis._abortController = undefined;\n\n\t\t// Retry via continue() - use setTimeout to break out of event handler chain\n\t\tsetTimeout(() => {\n\t\t\tthis.deps.continueAgent();\n\t\t}, 0);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Cancel in-progress retry.\n\t */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t\t// Note: _attempt is reset in the catch block of handleRetryableError\n\t\tthis.resolve();\n\t}\n\n\t/**\n\t * Wait for any in-progress retry to complete.\n\t * Returns immediately if no retry is in progress.\n\t */\n\tasync waitForRetry(): Promise<void> {\n\t\tif (!this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._promise;\n\t\tawait this.deps.waitForAgentIdle();\n\t}\n}\n"]}