@kenkaiiii/ggcoder 5.18.0 → 5.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-sidecar.js +64 -9
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +5 -2
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/shared.d.ts.map +1 -1
- package/dist/cli/shared.js +2 -0
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +7 -0
- package/dist/config.test.js.map +1 -1
- package/dist/core/agent-session-compaction.test.js +517 -9
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session-memory-tail.test.js +1 -1
- package/dist/core/agent-session-memory-tail.test.js.map +1 -1
- package/dist/core/agent-session-queue.test.js +1 -1
- package/dist/core/agent-session-queue.test.js.map +1 -1
- package/dist/core/agent-session-review-coverage.test.js +26 -0
- package/dist/core/agent-session-review-coverage.test.js.map +1 -1
- package/dist/core/agent-session-tool-result-policy.test.js +14 -1
- package/dist/core/agent-session-tool-result-policy.test.js.map +1 -1
- package/dist/core/agent-session.d.ts +32 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +229 -42
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-providers.d.ts.map +1 -1
- package/dist/core/auth-providers.js +14 -7
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/compaction/active-context.d.ts +17 -0
- package/dist/core/compaction/active-context.d.ts.map +1 -0
- package/dist/core/compaction/active-context.js +20 -0
- package/dist/core/compaction/active-context.js.map +1 -0
- package/dist/core/compaction/active-context.test.d.ts +2 -0
- package/dist/core/compaction/active-context.test.d.ts.map +1 -0
- package/dist/core/compaction/active-context.test.js +36 -0
- package/dist/core/compaction/active-context.test.js.map +1 -0
- package/dist/core/compaction/compactor.d.ts +18 -17
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +54 -37
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +101 -64
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/tool-result-pruner.d.ts +37 -0
- package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -0
- package/dist/core/compaction/tool-result-pruner.js +103 -0
- package/dist/core/compaction/tool-result-pruner.js.map +1 -0
- package/dist/core/compaction/tool-result-pruner.test.d.ts +2 -0
- package/dist/core/compaction/tool-result-pruner.test.d.ts.map +1 -0
- package/dist/core/compaction/tool-result-pruner.test.js +148 -0
- package/dist/core/compaction/tool-result-pruner.test.js.map +1 -0
- package/dist/core/ideal-review.d.ts +6 -0
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +14 -0
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/ideal-review.test.js +10 -1
- package/dist/core/ideal-review.test.js.map +1 -1
- package/dist/core/resolve-start.test.js +1 -0
- package/dist/core/resolve-start.test.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +1 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +7 -3
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +3 -16
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ModelSelector.d.ts.map +1 -1
- package/dist/ui/components/ModelSelector.js +1 -0
- package/dist/ui/components/ModelSelector.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +2 -4
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +6 -2
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.test.js +31 -0
- package/dist/ui/hooks/useAgentLoop.test.js.map +1 -1
- package/dist/ui/hooks/useContextCompaction.d.ts +5 -8
- package/dist/ui/hooks/useContextCompaction.d.ts.map +1 -1
- package/dist/ui/hooks/useContextCompaction.js +72 -14
- package/dist/ui/hooks/useContextCompaction.js.map +1 -1
- package/dist/ui/hooks/useContextCompaction.test.d.ts +2 -0
- package/dist/ui/hooks/useContextCompaction.test.d.ts.map +1 -0
- package/dist/ui/hooks/useContextCompaction.test.js +120 -0
- package/dist/ui/hooks/useContextCompaction.test.js.map +1 -0
- package/dist/ui/login.d.ts.map +1 -1
- package/dist/ui/login.js +2 -1
- package/dist/ui/login.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agentLoop, isAbortError, } from "@kenkaiiii/gg-agent";
|
|
1
|
+
import { agentLoop, isAbortError, isUsageLimitError, } from "@kenkaiiii/gg-agent";
|
|
2
2
|
import { ProviderError, } from "@kenkaiiii/gg-ai";
|
|
3
3
|
import { EventBus } from "./event-bus.js";
|
|
4
4
|
import { SlashCommandRegistry, createBuiltinCommands, } from "./slash-commands.js";
|
|
@@ -6,11 +6,12 @@ import { PROMPT_COMMANDS, getPromptCommand } from "./prompt-commands.js";
|
|
|
6
6
|
import { loadCustomCommands } from "./custom-commands.js";
|
|
7
7
|
import { SettingsManager } from "./settings-manager.js";
|
|
8
8
|
import { AuthStorage } from "./auth-storage.js";
|
|
9
|
+
import { MOONSHOT_OAUTH_KEY } from "@kenkaiiii/gg-core";
|
|
9
10
|
import { getClaudeCliUserAgent } from "./claude-code-version.js";
|
|
10
11
|
import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.js";
|
|
11
12
|
import { SessionManager, KEN_TURN_CUSTOM_KIND, AUTOPILOT_MARKER_CUSTOM_KIND, APP_MARKER_CUSTOM_KIND, } from "./session-manager.js";
|
|
12
13
|
import { ExtensionLoader } from "./extensions/loader.js";
|
|
13
|
-
import { shouldCompact, compact
|
|
14
|
+
import { shouldCompact, compact } from "./compaction/compactor.js";
|
|
14
15
|
import { getAuthStorageKeys, getContextWindow, getModel, getToolResultCharLimit, MODELS, } from "./model-registry.js";
|
|
15
16
|
import { discoverSkills } from "./skills.js";
|
|
16
17
|
import { ensureAppDirs } from "../config.js";
|
|
@@ -23,10 +24,12 @@ import { DeferredToolCatalog } from "./mcp/deferred-catalog.js";
|
|
|
23
24
|
import { createToolSearchTool } from "../tools/tool-search.js";
|
|
24
25
|
import { log } from "./logger.js";
|
|
25
26
|
import { setEstimatorModel } from "./compaction/token-estimator.js";
|
|
27
|
+
import { calculateActiveContextTokens } from "./compaction/active-context.js";
|
|
28
|
+
import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
26
29
|
import { discoverAgents } from "./agents.js";
|
|
27
30
|
import { enhancePrompt } from "../utils/prompt-enhancer.js";
|
|
28
31
|
import { detectProjectStack } from "./language-detector.js";
|
|
29
|
-
import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageMessage, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
|
|
32
|
+
import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageMessage, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
|
|
30
33
|
import { evaluateLoopBreak, buildLoopBreakMessage, ToolCallProgressTracker, detectTextRepetition, } from "./loop-breaker.js";
|
|
31
34
|
import { buildRegroundingMessage } from "./regrounding.js";
|
|
32
35
|
import { wrapSteeringText, STEERING_PREFIX } from "./steering.js";
|
|
@@ -40,6 +43,18 @@ export function resolveSessionToolResultCharLimit(model, provider, accountId) {
|
|
|
40
43
|
return (getToolResultCharLimit(model, { provider, accountId }) ??
|
|
41
44
|
Math.floor(getContextWindow(model, { provider, accountId }) * 3.5 * 0.3));
|
|
42
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Aggregate budget for ALL tool results produced in one assistant turn.
|
|
48
|
+
* Individual results are already capped, but wide parallel fan-outs (GPT-5.6's
|
|
49
|
+
* signature behavior) were observed injecting 100k+ uncached tokens in a single
|
|
50
|
+
* turn. ~15% of the context window in chars (1 token ≈ 3.5 chars), floored at
|
|
51
|
+
* 100KB so small windows still fit two full-size reads, ceilinged at 240KB so
|
|
52
|
+
* 1M-context models don't waive the budget entirely.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveSessionTurnToolResultCharLimit(model, provider, accountId) {
|
|
55
|
+
const contextChars = getContextWindow(model, { provider, accountId }) * 3.5;
|
|
56
|
+
return Math.max(100_000, Math.min(Math.floor(contextChars * 0.15), 240_000));
|
|
57
|
+
}
|
|
43
58
|
// ── Agent Session ──────────────────────────────────────────
|
|
44
59
|
export class AgentSession {
|
|
45
60
|
eventBus = new EventBus();
|
|
@@ -93,10 +108,16 @@ export class AgentSession {
|
|
|
93
108
|
hookFileEditCounts = new Map();
|
|
94
109
|
hookToolCalls = new Map();
|
|
95
110
|
idealReviewPhase = "idle";
|
|
111
|
+
/** Runtime-only suppression while Ken owns verification in autopilot mode. */
|
|
112
|
+
idealReviewSuppressed = false;
|
|
96
113
|
reviewCoverage;
|
|
97
114
|
loopBreakInjected = false;
|
|
98
115
|
regroundingInjected = false;
|
|
99
116
|
compactionOccurred = false;
|
|
117
|
+
lastCompactionCompacted = false;
|
|
118
|
+
compactionRetryAfter = 0;
|
|
119
|
+
/** Latest provider count, anchored to the assistant response it measured. */
|
|
120
|
+
providerContext = null;
|
|
100
121
|
originalRequest = "";
|
|
101
122
|
// Messages queued by the user while a run is in flight. Drained at the
|
|
102
123
|
// mid-loop steering boundary (user steering wins over the hooks), mirroring
|
|
@@ -634,6 +655,13 @@ export class AgentSession {
|
|
|
634
655
|
}
|
|
635
656
|
case "turn_end":
|
|
636
657
|
this.hookStats.turns = event.turn;
|
|
658
|
+
for (let index = this.messages.length - 1; index >= 0; index--) {
|
|
659
|
+
const anchor = this.messages[index];
|
|
660
|
+
if (anchor?.role === "assistant") {
|
|
661
|
+
this.providerContext = { usage: { ...event.usage }, anchor };
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
637
665
|
await this.persistTurnMetric(event);
|
|
638
666
|
break;
|
|
639
667
|
}
|
|
@@ -701,7 +729,7 @@ export class AgentSession {
|
|
|
701
729
|
const childCompletionFollowUp = buildSubAgentCompletionFollowUp(this.subAgentManager);
|
|
702
730
|
if (childCompletionFollowUp)
|
|
703
731
|
return childCompletionFollowUp;
|
|
704
|
-
if (this.opts.selfCorrectionHooks === false)
|
|
732
|
+
if (this.opts.selfCorrectionHooks === false || this.idealReviewSuppressed)
|
|
705
733
|
return null;
|
|
706
734
|
if (this.idealReviewPhase === "reviewing") {
|
|
707
735
|
const coverage = this.reviewCoverage.evidence();
|
|
@@ -746,7 +774,7 @@ export class AgentSession {
|
|
|
746
774
|
lspMissing: lspEvidence.missing,
|
|
747
775
|
});
|
|
748
776
|
return [
|
|
749
|
-
this.withReviewLspEvidence(buildIdealReviewMessage(decision.reasons, driftedFiles), lspEvidence),
|
|
777
|
+
this.withReviewLspEvidence(withReviewCoverageRequirements(buildIdealReviewMessage(decision.reasons, driftedFiles), coverage.missing), lspEvidence),
|
|
750
778
|
];
|
|
751
779
|
}
|
|
752
780
|
reviewLspEvidence(files) {
|
|
@@ -806,24 +834,45 @@ export class AgentSession {
|
|
|
806
834
|
this.lastAccountId = creds.accountId;
|
|
807
835
|
// Auto-compact if needed. This must happen after credential resolution so
|
|
808
836
|
// OpenAI OAuth/Codex sessions use the Codex product context window instead
|
|
809
|
-
// of the public API model window.
|
|
810
|
-
|
|
837
|
+
// of the public API model window. Failed/no-op attempts cool down across
|
|
838
|
+
// prompts; provider overflow recovery still bypasses this path entirely.
|
|
839
|
+
if (this.settingsManager.get("autoCompact") && Date.now() >= this.compactionRetryAfter) {
|
|
811
840
|
const contextWindow = getContextWindow(this.model, {
|
|
812
841
|
provider: this.provider,
|
|
813
842
|
accountId: creds.accountId,
|
|
814
843
|
});
|
|
815
844
|
const threshold = this.settingsManager.get("compactThreshold");
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
845
|
+
let activeTokens;
|
|
846
|
+
if (this.providerContext) {
|
|
847
|
+
const anchorIndex = this.messages.lastIndexOf(this.providerContext.anchor);
|
|
848
|
+
if (anchorIndex >= 0) {
|
|
849
|
+
activeTokens = calculateActiveContextTokens(this.messages, {
|
|
850
|
+
usage: this.providerContext.usage,
|
|
851
|
+
pendingMessages: this.messages.slice(anchorIndex + 1),
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
this.providerContext = null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (shouldCompact(this.messages, contextWindow, threshold, activeTokens)) {
|
|
859
|
+
try {
|
|
860
|
+
await this.compact(creds);
|
|
861
|
+
if (this.lastCompactionCompacted) {
|
|
862
|
+
// Re-grounding hook keys off this — the context was just summarized.
|
|
863
|
+
this.compactionOccurred = true;
|
|
864
|
+
this.compactionRetryAfter = 0;
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
this.compactionRetryAfter = Date.now() + 30_000;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
this.compactionRetryAfter = Date.now() + 30_000;
|
|
872
|
+
if (isAbortError(error) || this.opts.signal?.aborted)
|
|
873
|
+
throw error;
|
|
874
|
+
log("WARN", "compaction", `Pre-run compaction failed; cooling down for 30s: ${error instanceof Error ? error.message : String(error)}`);
|
|
875
|
+
}
|
|
827
876
|
}
|
|
828
877
|
}
|
|
829
878
|
const userAgent = this.provider === "anthropic" ? await getClaudeCliUserAgent() : undefined;
|
|
@@ -860,26 +909,93 @@ export class AgentSession {
|
|
|
860
909
|
// Codex caps each tool output at 10K tokens. Other transports retain the
|
|
861
910
|
// generic 30%-of-context allowance used before this provider policy.
|
|
862
911
|
maxToolResultChars: resolveSessionToolResultCharLimit(this.model, this.provider, accountId),
|
|
912
|
+
// Aggregate per-turn budget across parallel tool results (fan-out guard).
|
|
913
|
+
maxTurnToolResultChars: resolveSessionTurnToolResultCharLimit(this.model, this.provider, accountId),
|
|
863
914
|
// Self-correction hooks (same as the TUI): loop-break + re-grounding are
|
|
864
915
|
// polled mid-loop; the ideal review is polled when the agent would stop.
|
|
865
916
|
getSteeringMessages: () => this.getHookSteeringMessages(),
|
|
866
917
|
getFollowUpMessages: () => this.getHookFollowUpMessages(),
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
// loop retries with a smaller request, instead of surfacing the error.
|
|
871
|
-
// Without this the desktop app (which drives the loop through
|
|
872
|
-
// AgentSession, not the TUI's useContextCompaction hook) had NO auto
|
|
873
|
-
// recovery on 413 — the error went straight to the user. The non-force
|
|
874
|
-
// pre-call invocations pass through untouched: pre-turn compaction is
|
|
875
|
-
// already handled above, so we only act on the overflow force path.
|
|
876
|
-
// `loopMessages === this.messages` (prepareDynamicContext returns it by
|
|
877
|
-
// reference) and the post-loop `this.messages = loopMessages` re-sync
|
|
878
|
-
// keeps persistence correct after compact() swaps the array.
|
|
918
|
+
// Check authoritative provider usage before every model/tool step.
|
|
919
|
+
// Forced overflow recovery bypasses settings and cooldown; proactive
|
|
920
|
+
// checks honor both and estimate only messages unseen by the provider.
|
|
879
921
|
transformContext: async (messages, transformOpts) => {
|
|
880
|
-
if (
|
|
922
|
+
if (transformOpts.usage) {
|
|
923
|
+
const anchorIndex = messages.length - transformOpts.pendingMessages.length - 1;
|
|
924
|
+
const anchor = messages[anchorIndex];
|
|
925
|
+
if (anchor?.role === "assistant") {
|
|
926
|
+
this.providerContext = { usage: { ...transformOpts.usage }, anchor };
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const force = transformOpts.force === true;
|
|
930
|
+
if (!force) {
|
|
931
|
+
if (!this.settingsManager.get("autoCompact"))
|
|
932
|
+
return messages;
|
|
933
|
+
// Cheap stale-tool-output pruning before the expensive LLM
|
|
934
|
+
// compaction check. In-place mutation preserves anchors; drop the
|
|
935
|
+
// retained usage afterwards since it counted the pruned content.
|
|
936
|
+
const pruneResult = pruneStaleToolResults(messages);
|
|
937
|
+
if (pruneResult.pruned) {
|
|
938
|
+
this.providerContext = null;
|
|
939
|
+
log("INFO", "compaction", "Pruned stale tool outputs", {
|
|
940
|
+
prunedResults: String(pruneResult.prunedResults),
|
|
941
|
+
freedTokens: String(pruneResult.freedTokens),
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
if (Date.now() < this.compactionRetryAfter)
|
|
945
|
+
return messages;
|
|
946
|
+
// The turn's own usage also counted the pruned content — after a
|
|
947
|
+
// prune, fall back to estimating the (now smaller) history so the
|
|
948
|
+
// freed tokens actually defer the LLM compaction.
|
|
949
|
+
let usage = pruneResult.pruned ? undefined : transformOpts.usage;
|
|
950
|
+
let pendingMessages = transformOpts.pendingMessages;
|
|
951
|
+
if (!usage && this.providerContext) {
|
|
952
|
+
const anchorIndex = messages.lastIndexOf(this.providerContext.anchor);
|
|
953
|
+
if (anchorIndex >= 0) {
|
|
954
|
+
usage = this.providerContext.usage;
|
|
955
|
+
pendingMessages = messages.slice(anchorIndex + 1);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
this.providerContext = null;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const contextWindow = getContextWindow(this.model, {
|
|
962
|
+
provider: this.provider,
|
|
963
|
+
accountId,
|
|
964
|
+
});
|
|
965
|
+
const threshold = this.settingsManager.get("compactThreshold");
|
|
966
|
+
const activeTokens = calculateActiveContextTokens(messages, {
|
|
967
|
+
usage,
|
|
968
|
+
pendingMessages,
|
|
969
|
+
});
|
|
970
|
+
if (!shouldCompact(messages, contextWindow, threshold, activeTokens))
|
|
971
|
+
return messages;
|
|
972
|
+
}
|
|
973
|
+
// compact() operates on this.messages, while an earlier transform may
|
|
974
|
+
// have replaced the loop's in-flight array. Rebind before every attempt
|
|
975
|
+
// so the current tool results are included in the summary.
|
|
976
|
+
this.messages = messages;
|
|
977
|
+
try {
|
|
978
|
+
await this.compact({
|
|
979
|
+
accessToken: apiKey,
|
|
980
|
+
accountId,
|
|
981
|
+
projectId,
|
|
982
|
+
baseUrl: effectiveBaseUrl,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
catch (error) {
|
|
986
|
+
this.messages = messages;
|
|
987
|
+
this.compactionRetryAfter = Date.now() + 30_000;
|
|
988
|
+
if (force || isAbortError(error) || this.opts.signal?.aborted)
|
|
989
|
+
throw error;
|
|
990
|
+
log("WARN", "compaction", `In-flight compaction failed; cooling down for 30s: ${error instanceof Error ? error.message : String(error)}`);
|
|
991
|
+
return messages;
|
|
992
|
+
}
|
|
993
|
+
if (!this.lastCompactionCompacted) {
|
|
994
|
+
this.messages = messages;
|
|
995
|
+
this.compactionRetryAfter = Date.now() + 30_000;
|
|
881
996
|
return messages;
|
|
882
|
-
|
|
997
|
+
}
|
|
998
|
+
this.compactionRetryAfter = 0;
|
|
883
999
|
this.compactionOccurred = true;
|
|
884
1000
|
return this.messages;
|
|
885
1001
|
},
|
|
@@ -889,6 +1005,19 @@ export class AgentSession {
|
|
|
889
1005
|
this.eventBus.forwardAgentEvent(event);
|
|
890
1006
|
}
|
|
891
1007
|
};
|
|
1008
|
+
const clearInvalidStaticApiKey = async (error) => {
|
|
1009
|
+
if (!(error instanceof ProviderError) || error.statusCode !== 401)
|
|
1010
|
+
return false;
|
|
1011
|
+
if (!(await this.authStorage.isStaticApiKey(this.provider)))
|
|
1012
|
+
return false;
|
|
1013
|
+
// Clear whichever key actually resolved (the request may have used a
|
|
1014
|
+
// fallback key, not the model's first preference).
|
|
1015
|
+
const badKey = (await this.authStorage.pickStorageKey(this.currentAuthStorageKeys())) ??
|
|
1016
|
+
this.currentAuthStorageKeys()[0];
|
|
1017
|
+
log("WARN", "auth", `Got 401 for ${this.provider} (${badKey}) — API key is invalid or revoked`);
|
|
1018
|
+
await this.authStorage.clearCredentials(badKey);
|
|
1019
|
+
return true;
|
|
1020
|
+
};
|
|
892
1021
|
try {
|
|
893
1022
|
await runAgentLoop(creds.accessToken, creds.accountId, creds.projectId);
|
|
894
1023
|
}
|
|
@@ -897,21 +1026,48 @@ export class AgentSession {
|
|
|
897
1026
|
if (isAbortError(err) || this.opts.signal?.aborted) {
|
|
898
1027
|
return;
|
|
899
1028
|
}
|
|
900
|
-
|
|
1029
|
+
// Kimi OAuth plan ran out of usage (hard usage-limit stop, or an HTTP 402
|
|
1030
|
+
// billing stop). If the user ALSO configured a Moonshot API key, mark
|
|
1031
|
+
// the OAuth credential usage-exhausted (honoring the provider-stated
|
|
1032
|
+
// reset time when present) and retry this turn on the API key — OAuth
|
|
1033
|
+
// stays the preferred credential and resumes automatically once the mark
|
|
1034
|
+
// lapses. A generic 429 is deliberately excluded: it may be a transient
|
|
1035
|
+
// rate limit and must not silently switch the user to a billed API key.
|
|
1036
|
+
// Guarded on the Kimi managed endpoint actually being in use: if the API
|
|
1037
|
+
// key was already active, the same error means BOTH are out and must surface.
|
|
1038
|
+
if (this.provider === "moonshot" &&
|
|
1039
|
+
!this.baseUrl &&
|
|
1040
|
+
isKimiCodingEndpoint(creds.baseUrl) &&
|
|
1041
|
+
(isUsageLimitError(err) || (err instanceof ProviderError && err.statusCode === 402)) &&
|
|
1042
|
+
(await this.authStorage.hasCredentials("moonshot"))) {
|
|
1043
|
+
const resetsAt = err instanceof ProviderError ? err.resetsAt : undefined;
|
|
1044
|
+
await this.authStorage.markUsageExhausted(MOONSHOT_OAUTH_KEY, resetsAt);
|
|
1045
|
+
log("WARN", "auth", "Kimi OAuth usage limit reached — retrying this turn on the Moonshot API key", { resetsAt: resetsAt !== undefined ? String(resetsAt) : "unknown" });
|
|
1046
|
+
creds = await this.authStorage.resolveCredentials(this.provider, {
|
|
1047
|
+
storageKeys: this.currentAuthStorageKeys(),
|
|
1048
|
+
});
|
|
1049
|
+
this.lastAccountId = creds.accountId;
|
|
1050
|
+
// The runAgentLoop closure re-reads `creds`, so the retry picks up the
|
|
1051
|
+
// API key's baseUrl (api.moonshot.ai) and drops the Kimi coding headers.
|
|
1052
|
+
try {
|
|
1053
|
+
await runAgentLoop(creds.accessToken, creds.accountId, creds.projectId);
|
|
1054
|
+
}
|
|
1055
|
+
catch (fallbackErr) {
|
|
1056
|
+
// The fallback is inside this catch branch, so its errors do not pass
|
|
1057
|
+
// through the outer 401 handler. Clear a rejected API key explicitly
|
|
1058
|
+
// before surfacing the error and prompting the user to log in again.
|
|
1059
|
+
await clearInvalidStaticApiKey(fallbackErr);
|
|
1060
|
+
throw fallbackErr;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
else if (err instanceof ProviderError && err.statusCode === 401) {
|
|
901
1064
|
// Static API-key providers (GLM, Moonshot API key, etc.) have no refresh
|
|
902
1065
|
// mechanism — retrying with the same key is pointless. Clear the
|
|
903
1066
|
// credential and surface the error so the user re-logins. Kimi OAuth
|
|
904
1067
|
// (active for `moonshot` when present) is refreshable, so it falls
|
|
905
1068
|
// through to the force-refresh path below.
|
|
906
|
-
if (await
|
|
907
|
-
// Clear whichever key actually resolved (the request may have used
|
|
908
|
-
// a fallback key, not the model's first preference).
|
|
909
|
-
const badKey = (await this.authStorage.pickStorageKey(this.currentAuthStorageKeys())) ??
|
|
910
|
-
this.currentAuthStorageKeys()[0];
|
|
911
|
-
log("WARN", "auth", `Got 401 for ${this.provider} (${badKey}) — API key is invalid or revoked`);
|
|
912
|
-
await this.authStorage.clearCredentials(badKey);
|
|
1069
|
+
if (await clearInvalidStaticApiKey(err))
|
|
913
1070
|
throw err;
|
|
914
|
-
}
|
|
915
1071
|
log("INFO", "auth", "Got 401, force-refreshing token and retrying");
|
|
916
1072
|
creds = await this.authStorage.resolveCredentials(this.provider, {
|
|
917
1073
|
forceRefresh: true,
|
|
@@ -936,6 +1092,7 @@ export class AgentSession {
|
|
|
936
1092
|
if (provider)
|
|
937
1093
|
this.provider = provider;
|
|
938
1094
|
this.model = model;
|
|
1095
|
+
this.providerContext = null;
|
|
939
1096
|
// Keep host-provided option closures (notably chat delegation) aligned with
|
|
940
1097
|
// the live selection after an in-session model switch.
|
|
941
1098
|
this.opts.provider = this.provider;
|
|
@@ -1013,6 +1170,7 @@ export class AgentSession {
|
|
|
1013
1170
|
}
|
|
1014
1171
|
}
|
|
1015
1172
|
async compact(existingCredentials) {
|
|
1173
|
+
this.lastCompactionCompacted = false;
|
|
1016
1174
|
const creds = existingCredentials ??
|
|
1017
1175
|
(await this.authStorage.resolveCredentials(this.provider, {
|
|
1018
1176
|
storageKeys: this.currentAuthStorageKeys(),
|
|
@@ -1033,6 +1191,15 @@ export class AgentSession {
|
|
|
1033
1191
|
signal: this.opts.signal,
|
|
1034
1192
|
});
|
|
1035
1193
|
this.messages = result.messages;
|
|
1194
|
+
this.lastCompactionCompacted = result.result.compacted;
|
|
1195
|
+
if (!result.result.compacted) {
|
|
1196
|
+
this.eventBus.emit("compaction_end", {
|
|
1197
|
+
originalCount: result.result.originalCount,
|
|
1198
|
+
newCount: result.result.newCount,
|
|
1199
|
+
});
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
this.providerContext = null;
|
|
1036
1203
|
// Transient sessions (Ken chat/autopilot, subagent spawns) must NEVER touch
|
|
1037
1204
|
// the session store: without this guard, the first auto-compaction called
|
|
1038
1205
|
// sessionManager.create() and assigned a real sessionPath, silently turning
|
|
@@ -1183,6 +1350,18 @@ export class AgentSession {
|
|
|
1183
1350
|
getPlanMode() {
|
|
1184
1351
|
return this.planModeRef.current;
|
|
1185
1352
|
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Suppress only the pre-final Ideal self-review for this live session.
|
|
1355
|
+
* Autopilot uses this while Ken independently owns verification; loop-break
|
|
1356
|
+
* and post-compaction re-grounding remain active.
|
|
1357
|
+
*/
|
|
1358
|
+
setIdealReviewSuppressed(suppressed) {
|
|
1359
|
+
this.idealReviewSuppressed = suppressed;
|
|
1360
|
+
if (suppressed) {
|
|
1361
|
+
this.idealReviewPhase = "idle";
|
|
1362
|
+
this.reviewCoverage.reset();
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1186
1365
|
/** Queue a user message (optionally with attachments) to be injected mid-run
|
|
1187
1366
|
* as steering. Returns the new queue length. No-op semantics are the caller's
|
|
1188
1367
|
* concern. */
|
|
@@ -1631,7 +1810,15 @@ export class AgentSession {
|
|
|
1631
1810
|
provider: this.provider,
|
|
1632
1811
|
accountId: creds.accountId,
|
|
1633
1812
|
});
|
|
1634
|
-
|
|
1813
|
+
const needsLoadCompaction = this.settingsManager.get("autoCompact") &&
|
|
1814
|
+
shouldCompact(this.messages, contextWindow, this.settingsManager.get("compactThreshold"));
|
|
1815
|
+
if (needsLoadCompaction && this.opts.deferLoadCompaction) {
|
|
1816
|
+
// Host readiness is gated on initialize() — don't block it on a summary
|
|
1817
|
+
// LLM call (up to 30s). runLoop()'s pre-run auto-compaction picks this
|
|
1818
|
+
// up on the first prompt and emits compaction_start/_end for the UI.
|
|
1819
|
+
log("INFO", "session", "Restored session exceeds context — deferring compaction to first prompt");
|
|
1820
|
+
}
|
|
1821
|
+
else if (needsLoadCompaction) {
|
|
1635
1822
|
await this.subAgentManager?.hydrate(loaded.header.id);
|
|
1636
1823
|
log("INFO", "session", `Restored session exceeds context — auto-compacting`);
|
|
1637
1824
|
const compacted = await compact(this.messages, {
|