@prestyj/cli 5.6.0 → 5.8.0
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/README.md +2 -2
- package/dist/app-sidecar.js +369 -92
- 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 +33 -3
- 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 +521 -10
- 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.d.ts +2 -0
- package/dist/core/agent-session-review-coverage.test.d.ts.map +1 -0
- package/dist/core/agent-session-review-coverage.test.js +83 -0
- package/dist/core/agent-session-review-coverage.test.js.map +1 -0
- package/dist/core/agent-session-tool-result-policy.test.d.ts +2 -0
- package/dist/core/agent-session-tool-result-policy.test.d.ts.map +1 -0
- package/dist/core/agent-session-tool-result-policy.test.js +25 -0
- package/dist/core/agent-session-tool-result-policy.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +51 -12
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +415 -106
- 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 +15 -8
- 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 +19 -18
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +64 -42
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +122 -65
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/token-estimator.d.ts +25 -1
- package/dist/core/compaction/token-estimator.d.ts.map +1 -1
- package/dist/core/compaction/token-estimator.js +86 -1
- package/dist/core/compaction/token-estimator.js.map +1 -1
- package/dist/core/compaction/token-estimator.test.js +147 -1
- package/dist/core/compaction/token-estimator.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/event-bus.d.ts +2 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/ideal-review.d.ts +33 -0
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +78 -0
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/ideal-review.test.js +43 -1
- package/dist/core/ideal-review.test.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/lsp/client.d.ts +3 -0
- package/dist/core/lsp/client.d.ts.map +1 -1
- package/dist/core/lsp/client.js +20 -0
- package/dist/core/lsp/client.js.map +1 -1
- package/dist/core/lsp/manager.d.ts +32 -9
- package/dist/core/lsp/manager.d.ts.map +1 -1
- package/dist/core/lsp/manager.js +89 -37
- package/dist/core/lsp/manager.js.map +1 -1
- package/dist/core/lsp/manager.test.js +56 -4
- package/dist/core/lsp/manager.test.js.map +1 -1
- package/dist/core/project-discovery.d.ts +6 -9
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +35 -28
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.js +148 -0
- package/dist/core/project-discovery.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/run-lifecycle.d.ts +40 -0
- package/dist/core/run-lifecycle.d.ts.map +1 -0
- package/dist/core/run-lifecycle.js +95 -0
- package/dist/core/run-lifecycle.js.map +1 -0
- package/dist/core/run-lifecycle.test.d.ts +2 -0
- package/dist/core/run-lifecycle.test.d.ts.map +1 -0
- package/dist/core/run-lifecycle.test.js +64 -0
- package/dist/core/run-lifecycle.test.js.map +1 -0
- package/dist/core/session-compaction.d.ts +3 -0
- package/dist/core/session-compaction.d.ts.map +1 -1
- package/dist/core/session-compaction.js +14 -1
- package/dist/core/session-compaction.js.map +1 -1
- package/dist/core/session-compaction.test.js +5 -0
- package/dist/core/session-compaction.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +33 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +73 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +97 -2
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/session-preview.d.ts +11 -0
- package/dist/core/session-preview.d.ts.map +1 -0
- package/dist/core/session-preview.js +50 -0
- package/dist/core/session-preview.js.map +1 -0
- 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 +3 -2
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +42 -7
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +346 -30
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +201 -5
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/core/subagent-store.d.ts +20 -0
- package/dist/core/subagent-store.d.ts.map +1 -0
- package/dist/core/subagent-store.js +177 -0
- package/dist/core/subagent-store.js.map +1 -0
- package/dist/core/subagent-store.test.d.ts +2 -0
- package/dist/core/subagent-store.test.d.ts.map +1 -0
- package/dist/core/subagent-store.test.js +94 -0
- package/dist/core/subagent-store.test.js.map +1 -0
- package/dist/modes/agent-home-mode.d.ts.map +1 -1
- package/dist/modes/agent-home-mode.js +10 -3
- package/dist/modes/agent-home-mode.js.map +1 -1
- package/dist/modes/serve-mode.d.ts.map +1 -1
- package/dist/modes/serve-mode.js +10 -3
- package/dist/modes/serve-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +2 -0
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
- package/dist/modes/subagent-worker-mode.js +13 -3
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/tools/bash.d.ts +9 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +28 -18
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/bash.test.d.ts +2 -0
- package/dist/tools/bash.test.d.ts.map +1 -0
- package/dist/tools/bash.test.js +56 -0
- package/dist/tools/bash.test.js.map +1 -0
- package/dist/tools/goals.d.ts +3 -3
- package/dist/tools/overflow.d.ts +12 -2
- package/dist/tools/overflow.d.ts.map +1 -1
- package/dist/tools/overflow.js +49 -4
- package/dist/tools/overflow.js.map +1 -1
- package/dist/tools/overflow.test.d.ts +2 -0
- package/dist/tools/overflow.test.d.ts.map +1 -0
- package/dist/tools/overflow.test.js +62 -0
- package/dist/tools/overflow.test.js.map +1 -0
- package/dist/tools/subagent-shared.d.ts +8 -0
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +3 -8
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +10 -8
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagent.test.js +17 -1
- package/dist/tools/subagent.test.js.map +1 -1
- package/dist/tools/tasks.d.ts +2 -2
- package/dist/tools/web-fetch.d.ts +1 -1
- package/dist/ui/App.d.ts +7 -3
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +68 -98
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/Footer.d.ts +1 -0
- package/dist/ui/components/Footer.d.ts.map +1 -1
- package/dist/ui/components/Footer.js +3 -3
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/components/Footer.test.d.ts +2 -0
- package/dist/ui/components/Footer.test.d.ts.map +1 -0
- package/dist/ui/components/Footer.test.js +16 -0
- package/dist/ui/components/Footer.test.js.map +1 -0
- 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/components/SubAgentPanel.d.ts +2 -0
- package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
- package/dist/ui/components/SubAgentPanel.js +4 -2
- package/dist/ui/components/SubAgentPanel.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +9 -6
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +68 -8
- 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 +77 -16
- 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 +135 -0
- package/dist/ui/hooks/useContextCompaction.test.js.map +1 -0
- package/dist/ui/hooks/useSessionPersistence.d.ts +4 -2
- package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -1
- package/dist/ui/hooks/useSessionPersistence.js +10 -1
- package/dist/ui/hooks/useSessionPersistence.js.map +1 -1
- package/dist/ui/hooks/useTerminalTitle.d.ts +3 -3
- package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
- package/dist/ui/hooks/useTerminalTitle.js +5 -9
- package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
- package/dist/ui/login.d.ts.map +1 -1
- package/dist/ui/login.js +7 -2
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/render.d.ts +7 -2
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +4 -4
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/render.test.js +10 -1
- package/dist/ui/render.test.js.map +1 -1
- package/dist/ui/terminal-history.js +6 -2
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/ui/terminal-history.test.js +2 -2
- package/dist/ui/terminal-history.test.js.map +1 -1
- package/dist/ui/tui-history-parity.test.js +8 -1
- package/dist/ui/tui-history-parity.test.js.map +1 -1
- package/dist/utils/git.d.ts +2 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +12 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/git.test.d.ts +2 -0
- package/dist/utils/git.test.d.ts.map +1 -0
- package/dist/utils/git.test.js +36 -0
- package/dist/utils/git.test.js.map +1 -0
- package/package.json +5 -5
- package/dist/utils/session-title.d.ts +0 -19
- package/dist/utils/session-title.d.ts.map +0 -1
- package/dist/utils/session-title.js +0 -82
- package/dist/utils/session-title.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agentLoop, isAbortError } from "@prestyj/agent";
|
|
1
|
+
import { agentLoop, isAbortError, isUsageLimitError, } from "@prestyj/agent";
|
|
2
2
|
import { ProviderError, } from "@prestyj/ai";
|
|
3
3
|
import { EventBus } from "./event-bus.js";
|
|
4
4
|
import { SlashCommandRegistry, createBuiltinCommands, } from "./slash-commands.js";
|
|
@@ -6,33 +6,55 @@ 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 "@prestyj/core";
|
|
9
10
|
import { getClaudeCliUserAgent } from "./claude-code-version.js";
|
|
10
11
|
import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.js";
|
|
11
12
|
import { SessionManager, NOLAN_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 { getAuthStorageKeys, getContextWindow, getModel, MODELS } from "./model-registry.js";
|
|
14
|
+
import { shouldCompact, compact } from "./compaction/compactor.js";
|
|
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";
|
|
17
18
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
18
19
|
import { createTools, createWebSearchTool, } from "../tools/index.js";
|
|
20
|
+
import { buildSubAgentCompletionFollowUp } from "./subagent-manager.js";
|
|
19
21
|
import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
|
|
20
22
|
import { MCPClientManager, getAllMcpServers } from "./mcp/index.js";
|
|
21
23
|
import { DeferredToolCatalog } from "./mcp/deferred-catalog.js";
|
|
22
24
|
import { createToolSearchTool } from "../tools/tool-search.js";
|
|
23
25
|
import { log } from "./logger.js";
|
|
24
|
-
import { setEstimatorModel } from "./compaction/token-estimator.js";
|
|
26
|
+
import { setEstimatorModel, calibrateEstimatorFromUsage } from "./compaction/token-estimator.js";
|
|
27
|
+
import { calculateActiveContextTokens } from "./compaction/active-context.js";
|
|
28
|
+
import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
25
29
|
import { discoverAgents } from "./agents.js";
|
|
26
|
-
import { generateSessionTitle } from "../utils/session-title.js";
|
|
27
30
|
import { enhancePrompt } from "../utils/prompt-enhancer.js";
|
|
28
31
|
import { detectProjectStack } from "./language-detector.js";
|
|
29
|
-
import { evaluateIdealReview, buildIdealReviewMessage, detectTestDrift, } 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";
|
|
36
|
+
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
33
37
|
import crypto from "node:crypto";
|
|
34
38
|
import fs from "node:fs/promises";
|
|
35
39
|
import path from "node:path";
|
|
40
|
+
// ── Tool-result policy ─────────────────────────────────────
|
|
41
|
+
/** Resolve the per-result cap passed to the agent loop for the active transport. */
|
|
42
|
+
export function resolveSessionToolResultCharLimit(model, provider, accountId) {
|
|
43
|
+
return (getToolResultCharLimit(model, { provider, accountId }) ??
|
|
44
|
+
Math.floor(getContextWindow(model, { provider, accountId }) * 3.5 * 0.3));
|
|
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
|
+
}
|
|
36
58
|
// ── Agent Session ──────────────────────────────────────────
|
|
37
59
|
export class AgentSession {
|
|
38
60
|
eventBus = new EventBus();
|
|
@@ -59,6 +81,7 @@ export class AgentSession {
|
|
|
59
81
|
// display only, persisted + reloaded so a resumed session shows the same
|
|
60
82
|
// transcript rows the live run showed.
|
|
61
83
|
appMarkers = [];
|
|
84
|
+
turnMetrics = [];
|
|
62
85
|
tools = [];
|
|
63
86
|
/** Rebuilds the read tool for a new model (video byte cap is baked in at
|
|
64
87
|
* creation). Called from switchModel so video-capable models get the
|
|
@@ -84,10 +107,17 @@ export class AgentSession {
|
|
|
84
107
|
hookProgressTracker = new ToolCallProgressTracker();
|
|
85
108
|
hookFileEditCounts = new Map();
|
|
86
109
|
hookToolCalls = new Map();
|
|
87
|
-
|
|
110
|
+
idealReviewPhase = "idle";
|
|
111
|
+
/** Runtime-only suppression while Nolan owns verification in autopilot mode. */
|
|
112
|
+
idealReviewSuppressed = false;
|
|
113
|
+
reviewCoverage;
|
|
88
114
|
loopBreakInjected = false;
|
|
89
115
|
regroundingInjected = false;
|
|
90
116
|
compactionOccurred = false;
|
|
117
|
+
lastCompactionCompacted = false;
|
|
118
|
+
compactionRetryAfter = 0;
|
|
119
|
+
/** Latest provider count, anchored to the assistant response it measured. */
|
|
120
|
+
providerContext = null;
|
|
91
121
|
originalRequest = "";
|
|
92
122
|
// Messages queued by the user while a run is in flight. Drained at the
|
|
93
123
|
// mid-loop steering boundary (user steering wins over the hooks), mirroring
|
|
@@ -125,6 +155,10 @@ export class AgentSession {
|
|
|
125
155
|
* model emits step-completion markers the UI's plan-progress widget reads. */
|
|
126
156
|
approvedPlanPath;
|
|
127
157
|
sessionId = "";
|
|
158
|
+
/** Stable identity shared by compaction and approved-plan checkpoint files. */
|
|
159
|
+
conversationId = "";
|
|
160
|
+
/** Original user-authored prompt, retained when internal messages replace history. */
|
|
161
|
+
sessionPreview = "";
|
|
128
162
|
/** Runtime conversation identity for provider transport headers. Transient
|
|
129
163
|
* children need one even though they intentionally have no persisted session. */
|
|
130
164
|
transportSessionId = crypto.randomUUID();
|
|
@@ -138,6 +172,7 @@ export class AgentSession {
|
|
|
138
172
|
this.provider = options.provider;
|
|
139
173
|
this.model = options.model;
|
|
140
174
|
this.cwd = options.cwd;
|
|
175
|
+
this.reviewCoverage = new ReviewCoverageTracker(this.cwd);
|
|
141
176
|
this.baseUrl = options.baseUrl;
|
|
142
177
|
this.maxTokens = this.resolveMaxTokens(options.model);
|
|
143
178
|
this.thinkingLevel = options.thinkingLevel;
|
|
@@ -203,6 +238,12 @@ export class AgentSession {
|
|
|
203
238
|
model: this.model,
|
|
204
239
|
lspDiagnostics: this.settingsManager.get("lspDiagnostics"),
|
|
205
240
|
authStorage: this.authStorage,
|
|
241
|
+
onFileRead: (filePath) => this.reviewCoverage.recordRead(filePath),
|
|
242
|
+
onFileMutated: (filePath) => {
|
|
243
|
+
const relative = path.relative(this.cwd, filePath) || path.basename(filePath);
|
|
244
|
+
this.hookFileEditCounts.set(relative, (this.hookFileEditCounts.get(relative) ?? 0) + 1);
|
|
245
|
+
this.reviewCoverage.recordChanged(filePath);
|
|
246
|
+
},
|
|
206
247
|
// Lazy — sessionId/model/provider can change after createTools() runs, so
|
|
207
248
|
// sub-agent spawns read the current parent state at execution time.
|
|
208
249
|
getProvider: () => this.provider,
|
|
@@ -282,6 +323,8 @@ export class AgentSession {
|
|
|
282
323
|
else {
|
|
283
324
|
await this.createNewSession();
|
|
284
325
|
}
|
|
326
|
+
if (this.sessionId)
|
|
327
|
+
await this.subAgentManager?.hydrate(this.sessionId);
|
|
285
328
|
// EZ Coder owns its command registry. Other agents start with an isolated
|
|
286
329
|
// empty registry and can register their own commands in their own file.
|
|
287
330
|
if (this.opts.coderSlashCommands !== false) {
|
|
@@ -576,7 +619,8 @@ export class AgentSession {
|
|
|
576
619
|
this.hookProgressTracker.reset();
|
|
577
620
|
this.hookFileEditCounts.clear();
|
|
578
621
|
this.hookToolCalls.clear();
|
|
579
|
-
this.
|
|
622
|
+
this.reviewCoverage.reset();
|
|
623
|
+
this.idealReviewPhase = "idle";
|
|
580
624
|
this.loopBreakInjected = false;
|
|
581
625
|
this.regroundingInjected = false;
|
|
582
626
|
this.compactionOccurred = false;
|
|
@@ -587,7 +631,7 @@ export class AgentSession {
|
|
|
587
631
|
* the same signals the TUI's useAgentLoop collects, so the loop-break and
|
|
588
632
|
* ideal-review decisions match across the CLI and the app.
|
|
589
633
|
*/
|
|
590
|
-
trackHookEvent(event) {
|
|
634
|
+
async trackHookEvent(event) {
|
|
591
635
|
switch (event.type) {
|
|
592
636
|
case "text_delta":
|
|
593
637
|
this.hookText += event.text;
|
|
@@ -610,13 +654,6 @@ export class AgentSession {
|
|
|
610
654
|
this.hookStats.bashCalls += 1;
|
|
611
655
|
this.hookConsecutiveFailures = event.isError ? this.hookConsecutiveFailures + 1 : 0;
|
|
612
656
|
this.hookRepeatedNoProgressCalls = this.hookProgressTracker.record(name, args, event.result, event.isError);
|
|
613
|
-
if ((name === "edit" || name === "write") && args) {
|
|
614
|
-
const filePath = args.file_path;
|
|
615
|
-
if (typeof filePath === "string") {
|
|
616
|
-
const fileNext = (this.hookFileEditCounts.get(filePath) ?? 0) + 1;
|
|
617
|
-
this.hookFileEditCounts.set(filePath, fileNext);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
657
|
if (name === "edit" && !event.isError) {
|
|
621
658
|
const diff = event.details?.diff ?? event.result;
|
|
622
659
|
const added = (diff.match(/^\+[^+]/gm) ?? []).length;
|
|
@@ -627,6 +664,14 @@ export class AgentSession {
|
|
|
627
664
|
}
|
|
628
665
|
case "turn_end":
|
|
629
666
|
this.hookStats.turns = event.turn;
|
|
667
|
+
for (let index = this.messages.length - 1; index >= 0; index--) {
|
|
668
|
+
const anchor = this.messages[index];
|
|
669
|
+
if (anchor?.role === "assistant") {
|
|
670
|
+
this.providerContext = { usage: { ...event.usage }, anchor };
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
await this.persistTurnMetric(event);
|
|
630
675
|
break;
|
|
631
676
|
}
|
|
632
677
|
}
|
|
@@ -686,15 +731,35 @@ export class AgentSession {
|
|
|
686
731
|
return null;
|
|
687
732
|
}
|
|
688
733
|
/**
|
|
689
|
-
* Pre-stop
|
|
690
|
-
*
|
|
734
|
+
* Pre-stop Ideal review phase machine. Once review starts, completion is
|
|
735
|
+
* blocked until harness-owned post-injection reads cover every changed file.
|
|
691
736
|
*/
|
|
692
737
|
getHookFollowUpMessages() {
|
|
693
|
-
|
|
738
|
+
const childCompletionFollowUp = buildSubAgentCompletionFollowUp(this.subAgentManager);
|
|
739
|
+
if (childCompletionFollowUp)
|
|
740
|
+
return childCompletionFollowUp;
|
|
741
|
+
if (this.opts.selfCorrectionHooks === false || this.idealReviewSuppressed)
|
|
694
742
|
return null;
|
|
695
|
-
if (
|
|
743
|
+
if (this.idealReviewPhase === "reviewing") {
|
|
744
|
+
const coverage = this.reviewCoverage.evidence();
|
|
745
|
+
const lspEvidence = this.reviewLspEvidence(coverage.expected);
|
|
746
|
+
log("INFO", "ideal", "Ideal review coverage check", {
|
|
747
|
+
covered: coverage.covered,
|
|
748
|
+
missing: coverage.missing,
|
|
749
|
+
lspLowConfidence: lspEvidence.lowConfidence,
|
|
750
|
+
lspMissing: lspEvidence.missing,
|
|
751
|
+
});
|
|
752
|
+
if (coverage.missing.length > 0) {
|
|
753
|
+
return [
|
|
754
|
+
this.withReviewLspEvidence(buildReviewCoverageMessage(coverage.missing), lspEvidence),
|
|
755
|
+
];
|
|
756
|
+
}
|
|
757
|
+
this.idealReviewPhase = "complete";
|
|
696
758
|
return null;
|
|
697
|
-
|
|
759
|
+
}
|
|
760
|
+
if (this.idealReviewPhase === "complete")
|
|
761
|
+
return null;
|
|
762
|
+
if (!this.settingsManager.get("idealReviewEnabled"))
|
|
698
763
|
return null;
|
|
699
764
|
const decision = evaluateIdealReview(this.hookStats);
|
|
700
765
|
// Test drift fires the review even on a small change the score would skip:
|
|
@@ -702,9 +767,50 @@ export class AgentSession {
|
|
|
702
767
|
const driftedFiles = detectTestDrift(this.hookFileEditCounts.keys(), this.cwd).slice(0, 5);
|
|
703
768
|
if (!decision.shouldReview && driftedFiles.length === 0)
|
|
704
769
|
return null;
|
|
705
|
-
this.
|
|
706
|
-
this.
|
|
707
|
-
|
|
770
|
+
this.reviewCoverage.start(this.hookFileEditCounts.keys());
|
|
771
|
+
this.idealReviewPhase = "reviewing";
|
|
772
|
+
const coverage = this.reviewCoverage.evidence();
|
|
773
|
+
const lspEvidence = this.reviewLspEvidence(coverage.expected);
|
|
774
|
+
this.eventBus.emit("hook", {
|
|
775
|
+
kind: "ideal",
|
|
776
|
+
coverageExpected: coverage.expected,
|
|
777
|
+
coverageMissing: coverage.missing,
|
|
778
|
+
});
|
|
779
|
+
log("INFO", "ideal", "Injecting ideal review before final response", {
|
|
780
|
+
coverageExpected: coverage.expected,
|
|
781
|
+
coverageMissing: coverage.missing,
|
|
782
|
+
lspLowConfidence: lspEvidence.lowConfidence,
|
|
783
|
+
lspMissing: lspEvidence.missing,
|
|
784
|
+
});
|
|
785
|
+
return [
|
|
786
|
+
this.withReviewLspEvidence(withReviewCoverageRequirements(buildIdealReviewMessage(decision.reasons, driftedFiles), coverage.missing), lspEvidence),
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
reviewLspEvidence(files) {
|
|
790
|
+
const lowConfidence = [];
|
|
791
|
+
const missing = [];
|
|
792
|
+
for (const filePath of files) {
|
|
793
|
+
const outcome = this.lspManager?.getLatestOutcome(filePath);
|
|
794
|
+
if (outcome?.kind === "low_confidence")
|
|
795
|
+
lowConfidence.push(filePath);
|
|
796
|
+
else if (outcome?.kind !== "clean" && outcome?.kind !== "diagnostics")
|
|
797
|
+
missing.push(filePath);
|
|
798
|
+
}
|
|
799
|
+
return { lowConfidence, missing };
|
|
800
|
+
}
|
|
801
|
+
withReviewLspEvidence(message, evidence) {
|
|
802
|
+
if (evidence.lowConfidence.length === 0 && evidence.missing.length === 0)
|
|
803
|
+
return message;
|
|
804
|
+
const notes = [
|
|
805
|
+
...(evidence.lowConfidence.length > 0
|
|
806
|
+
? [`Diagnostics are low confidence while indexing: ${evidence.lowConfidence.join(", ")}.`]
|
|
807
|
+
: []),
|
|
808
|
+
...(evidence.missing.length > 0
|
|
809
|
+
? [`Diagnostics evidence is unavailable or missing: ${evidence.missing.join(", ")}.`]
|
|
810
|
+
: []),
|
|
811
|
+
"Do not describe those files as compiler-clean without other evidence.",
|
|
812
|
+
];
|
|
813
|
+
return { role: "user", content: `${String(message.content)}\n\n${notes.join(" ")}` };
|
|
708
814
|
}
|
|
709
815
|
/** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
|
|
710
816
|
async runLoop() {
|
|
@@ -737,24 +843,45 @@ export class AgentSession {
|
|
|
737
843
|
this.lastAccountId = creds.accountId;
|
|
738
844
|
// Auto-compact if needed. This must happen after credential resolution so
|
|
739
845
|
// OpenAI OAuth/Codex sessions use the Codex product context window instead
|
|
740
|
-
// of the public API model window.
|
|
741
|
-
|
|
846
|
+
// of the public API model window. Failed/no-op attempts cool down across
|
|
847
|
+
// prompts; provider overflow recovery still bypasses this path entirely.
|
|
848
|
+
if (this.settingsManager.get("autoCompact") && Date.now() >= this.compactionRetryAfter) {
|
|
742
849
|
const contextWindow = getContextWindow(this.model, {
|
|
743
850
|
provider: this.provider,
|
|
744
851
|
accountId: creds.accountId,
|
|
745
852
|
});
|
|
746
853
|
const threshold = this.settingsManager.get("compactThreshold");
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
854
|
+
let activeTokens;
|
|
855
|
+
if (this.providerContext) {
|
|
856
|
+
const anchorIndex = this.messages.lastIndexOf(this.providerContext.anchor);
|
|
857
|
+
if (anchorIndex >= 0) {
|
|
858
|
+
activeTokens = calculateActiveContextTokens(this.messages, {
|
|
859
|
+
usage: this.providerContext.usage,
|
|
860
|
+
pendingMessages: this.messages.slice(anchorIndex + 1),
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
this.providerContext = null;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (shouldCompact(this.messages, contextWindow, threshold, activeTokens)) {
|
|
868
|
+
try {
|
|
869
|
+
await this.compact(creds);
|
|
870
|
+
if (this.lastCompactionCompacted) {
|
|
871
|
+
// Re-grounding hook keys off this — the context was just summarized.
|
|
872
|
+
this.compactionOccurred = true;
|
|
873
|
+
this.compactionRetryAfter = 0;
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
this.compactionRetryAfter = Date.now() + 30_000;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
this.compactionRetryAfter = Date.now() + 30_000;
|
|
881
|
+
if (isAbortError(error) || this.opts.signal?.aborted)
|
|
882
|
+
throw error;
|
|
883
|
+
log("WARN", "compaction", `Pre-run compaction failed; cooling down for 30s: ${error instanceof Error ? error.message : String(error)}`);
|
|
884
|
+
}
|
|
758
885
|
}
|
|
759
886
|
}
|
|
760
887
|
const userAgent = this.provider === "anthropic" ? await getClaudeCliUserAgent() : undefined;
|
|
@@ -788,38 +915,121 @@ export class AgentSession {
|
|
|
788
915
|
supportsImages: modelInfo?.supportsImages,
|
|
789
916
|
supportsVideo: modelInfo?.supportsVideo,
|
|
790
917
|
userAgent,
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
maxToolResultChars:
|
|
918
|
+
// Codex caps each tool output at 10K tokens. Other transports retain the
|
|
919
|
+
// generic 30%-of-context allowance used before this provider policy.
|
|
920
|
+
maxToolResultChars: resolveSessionToolResultCharLimit(this.model, this.provider, accountId),
|
|
921
|
+
// Aggregate per-turn budget across parallel tool results (fan-out guard).
|
|
922
|
+
maxTurnToolResultChars: resolveSessionTurnToolResultCharLimit(this.model, this.provider, accountId),
|
|
794
923
|
// Self-correction hooks (same as the TUI): loop-break + re-grounding are
|
|
795
924
|
// polled mid-loop; the ideal review is polled when the agent would stop.
|
|
796
925
|
getSteeringMessages: () => this.getHookSteeringMessages(),
|
|
797
926
|
getFollowUpMessages: () => this.getHookFollowUpMessages(),
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
// loop retries with a smaller request, instead of surfacing the error.
|
|
802
|
-
// Without this the desktop app (which drives the loop through
|
|
803
|
-
// AgentSession, not the TUI's useContextCompaction hook) had NO auto
|
|
804
|
-
// recovery on 413 — the error went straight to the user. The non-force
|
|
805
|
-
// pre-call invocations pass through untouched: pre-turn compaction is
|
|
806
|
-
// already handled above, so we only act on the overflow force path.
|
|
807
|
-
// `loopMessages === this.messages` (prepareDynamicContext returns it by
|
|
808
|
-
// reference) and the post-loop `this.messages = loopMessages` re-sync
|
|
809
|
-
// keeps persistence correct after compact() swaps the array.
|
|
927
|
+
// Check authoritative provider usage before every model/tool step.
|
|
928
|
+
// Forced overflow recovery bypasses settings and cooldown; proactive
|
|
929
|
+
// checks honor both and estimate only messages unseen by the provider.
|
|
810
930
|
transformContext: async (messages, transformOpts) => {
|
|
811
|
-
if (
|
|
931
|
+
if (transformOpts.usage) {
|
|
932
|
+
const anchorIndex = messages.length - transformOpts.pendingMessages.length - 1;
|
|
933
|
+
const anchor = messages[anchorIndex];
|
|
934
|
+
if (anchor?.role === "assistant") {
|
|
935
|
+
this.providerContext = { usage: { ...transformOpts.usage }, anchor };
|
|
936
|
+
// Feed the authoritative usage back into the token estimator so
|
|
937
|
+
// char-based estimates track this session's real tokenizer.
|
|
938
|
+
calibrateEstimatorFromUsage(messages.slice(0, anchorIndex), transformOpts.usage);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const force = transformOpts.force === true;
|
|
942
|
+
if (!force) {
|
|
943
|
+
if (!this.settingsManager.get("autoCompact"))
|
|
944
|
+
return messages;
|
|
945
|
+
// Cheap stale-tool-output pruning before the expensive LLM
|
|
946
|
+
// compaction check. In-place mutation preserves anchors; drop the
|
|
947
|
+
// retained usage afterwards since it counted the pruned content.
|
|
948
|
+
const pruneResult = pruneStaleToolResults(messages);
|
|
949
|
+
if (pruneResult.pruned) {
|
|
950
|
+
this.providerContext = null;
|
|
951
|
+
log("INFO", "compaction", "Pruned stale tool outputs", {
|
|
952
|
+
prunedResults: String(pruneResult.prunedResults),
|
|
953
|
+
freedTokens: String(pruneResult.freedTokens),
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
if (Date.now() < this.compactionRetryAfter)
|
|
957
|
+
return messages;
|
|
958
|
+
// The turn's own usage also counted the pruned content — after a
|
|
959
|
+
// prune, fall back to estimating the (now smaller) history so the
|
|
960
|
+
// freed tokens actually defer the LLM compaction.
|
|
961
|
+
let usage = pruneResult.pruned ? undefined : transformOpts.usage;
|
|
962
|
+
let pendingMessages = transformOpts.pendingMessages;
|
|
963
|
+
if (!usage && this.providerContext) {
|
|
964
|
+
const anchorIndex = messages.lastIndexOf(this.providerContext.anchor);
|
|
965
|
+
if (anchorIndex >= 0) {
|
|
966
|
+
usage = this.providerContext.usage;
|
|
967
|
+
pendingMessages = messages.slice(anchorIndex + 1);
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
this.providerContext = null;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const contextWindow = getContextWindow(this.model, {
|
|
974
|
+
provider: this.provider,
|
|
975
|
+
accountId,
|
|
976
|
+
});
|
|
977
|
+
const threshold = this.settingsManager.get("compactThreshold");
|
|
978
|
+
const activeTokens = calculateActiveContextTokens(messages, {
|
|
979
|
+
usage,
|
|
980
|
+
pendingMessages,
|
|
981
|
+
});
|
|
982
|
+
if (!shouldCompact(messages, contextWindow, threshold, activeTokens))
|
|
983
|
+
return messages;
|
|
984
|
+
}
|
|
985
|
+
// compact() operates on this.messages, while an earlier transform may
|
|
986
|
+
// have replaced the loop's in-flight array. Rebind before every attempt
|
|
987
|
+
// so the current tool results are included in the summary.
|
|
988
|
+
this.messages = messages;
|
|
989
|
+
try {
|
|
990
|
+
await this.compact({
|
|
991
|
+
accessToken: apiKey,
|
|
992
|
+
accountId,
|
|
993
|
+
projectId,
|
|
994
|
+
baseUrl: effectiveBaseUrl,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
catch (error) {
|
|
998
|
+
this.messages = messages;
|
|
999
|
+
this.compactionRetryAfter = Date.now() + 30_000;
|
|
1000
|
+
if (force || isAbortError(error) || this.opts.signal?.aborted)
|
|
1001
|
+
throw error;
|
|
1002
|
+
log("WARN", "compaction", `In-flight compaction failed; cooling down for 30s: ${error instanceof Error ? error.message : String(error)}`);
|
|
812
1003
|
return messages;
|
|
813
|
-
|
|
1004
|
+
}
|
|
1005
|
+
if (!this.lastCompactionCompacted) {
|
|
1006
|
+
this.messages = messages;
|
|
1007
|
+
this.compactionRetryAfter = Date.now() + 30_000;
|
|
1008
|
+
return messages;
|
|
1009
|
+
}
|
|
1010
|
+
this.compactionRetryAfter = 0;
|
|
814
1011
|
this.compactionOccurred = true;
|
|
815
1012
|
return this.messages;
|
|
816
1013
|
},
|
|
817
1014
|
});
|
|
818
1015
|
for await (const event of generator) {
|
|
819
|
-
this.trackHookEvent(event);
|
|
1016
|
+
await this.trackHookEvent(event);
|
|
820
1017
|
this.eventBus.forwardAgentEvent(event);
|
|
821
1018
|
}
|
|
822
1019
|
};
|
|
1020
|
+
const clearInvalidStaticApiKey = async (error) => {
|
|
1021
|
+
if (!(error instanceof ProviderError) || error.statusCode !== 401)
|
|
1022
|
+
return false;
|
|
1023
|
+
if (!(await this.authStorage.isStaticApiKey(this.provider)))
|
|
1024
|
+
return false;
|
|
1025
|
+
// Clear whichever key actually resolved (the request may have used a
|
|
1026
|
+
// fallback key, not the model's first preference).
|
|
1027
|
+
const badKey = (await this.authStorage.pickStorageKey(this.currentAuthStorageKeys())) ??
|
|
1028
|
+
this.currentAuthStorageKeys()[0];
|
|
1029
|
+
log("WARN", "auth", `Got 401 for ${this.provider} (${badKey}) — API key is invalid or revoked`);
|
|
1030
|
+
await this.authStorage.clearCredentials(badKey);
|
|
1031
|
+
return true;
|
|
1032
|
+
};
|
|
823
1033
|
try {
|
|
824
1034
|
await runAgentLoop(creds.accessToken, creds.accountId, creds.projectId);
|
|
825
1035
|
}
|
|
@@ -828,21 +1038,48 @@ export class AgentSession {
|
|
|
828
1038
|
if (isAbortError(err) || this.opts.signal?.aborted) {
|
|
829
1039
|
return;
|
|
830
1040
|
}
|
|
831
|
-
|
|
1041
|
+
// Kimi OAuth plan ran out of usage (hard usage-limit stop, or an HTTP 402
|
|
1042
|
+
// billing stop). If the user ALSO configured a Moonshot API key, mark
|
|
1043
|
+
// the OAuth credential usage-exhausted (honoring the provider-stated
|
|
1044
|
+
// reset time when present) and retry this turn on the API key — OAuth
|
|
1045
|
+
// stays the preferred credential and resumes automatically once the mark
|
|
1046
|
+
// lapses. A generic 429 is deliberately excluded: it may be a transient
|
|
1047
|
+
// rate limit and must not silently switch the user to a billed API key.
|
|
1048
|
+
// Guarded on the Kimi managed endpoint actually being in use: if the API
|
|
1049
|
+
// key was already active, the same error means BOTH are out and must surface.
|
|
1050
|
+
if (this.provider === "moonshot" &&
|
|
1051
|
+
!this.baseUrl &&
|
|
1052
|
+
isKimiCodingEndpoint(creds.baseUrl) &&
|
|
1053
|
+
(isUsageLimitError(err) || (err instanceof ProviderError && err.statusCode === 402)) &&
|
|
1054
|
+
(await this.authStorage.hasCredentials("moonshot"))) {
|
|
1055
|
+
const resetsAt = err instanceof ProviderError ? err.resetsAt : undefined;
|
|
1056
|
+
await this.authStorage.markUsageExhausted(MOONSHOT_OAUTH_KEY, resetsAt);
|
|
1057
|
+
log("WARN", "auth", "Kimi OAuth usage limit reached — retrying this turn on the Moonshot API key", { resetsAt: resetsAt !== undefined ? String(resetsAt) : "unknown" });
|
|
1058
|
+
creds = await this.authStorage.resolveCredentials(this.provider, {
|
|
1059
|
+
storageKeys: this.currentAuthStorageKeys(),
|
|
1060
|
+
});
|
|
1061
|
+
this.lastAccountId = creds.accountId;
|
|
1062
|
+
// The runAgentLoop closure re-reads `creds`, so the retry picks up the
|
|
1063
|
+
// API key's baseUrl (api.moonshot.ai) and drops the Kimi coding headers.
|
|
1064
|
+
try {
|
|
1065
|
+
await runAgentLoop(creds.accessToken, creds.accountId, creds.projectId);
|
|
1066
|
+
}
|
|
1067
|
+
catch (fallbackErr) {
|
|
1068
|
+
// The fallback is inside this catch branch, so its errors do not pass
|
|
1069
|
+
// through the outer 401 handler. Clear a rejected API key explicitly
|
|
1070
|
+
// before surfacing the error and prompting the user to log in again.
|
|
1071
|
+
await clearInvalidStaticApiKey(fallbackErr);
|
|
1072
|
+
throw fallbackErr;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
else if (err instanceof ProviderError && err.statusCode === 401) {
|
|
832
1076
|
// Static API-key providers (GLM, Moonshot API key, etc.) have no refresh
|
|
833
1077
|
// mechanism — retrying with the same key is pointless. Clear the
|
|
834
1078
|
// credential and surface the error so the user re-logins. Kimi OAuth
|
|
835
1079
|
// (active for `moonshot` when present) is refreshable, so it falls
|
|
836
1080
|
// through to the force-refresh path below.
|
|
837
|
-
if (await
|
|
838
|
-
// Clear whichever key actually resolved (the request may have used
|
|
839
|
-
// a fallback key, not the model's first preference).
|
|
840
|
-
const badKey = (await this.authStorage.pickStorageKey(this.currentAuthStorageKeys())) ??
|
|
841
|
-
this.currentAuthStorageKeys()[0];
|
|
842
|
-
log("WARN", "auth", `Got 401 for ${this.provider} (${badKey}) — API key is invalid or revoked`);
|
|
843
|
-
await this.authStorage.clearCredentials(badKey);
|
|
1081
|
+
if (await clearInvalidStaticApiKey(err))
|
|
844
1082
|
throw err;
|
|
845
|
-
}
|
|
846
1083
|
log("INFO", "auth", "Got 401, force-refreshing token and retrying");
|
|
847
1084
|
creds = await this.authStorage.resolveCredentials(this.provider, {
|
|
848
1085
|
forceRefresh: true,
|
|
@@ -867,6 +1104,7 @@ export class AgentSession {
|
|
|
867
1104
|
if (provider)
|
|
868
1105
|
this.provider = provider;
|
|
869
1106
|
this.model = model;
|
|
1107
|
+
this.providerContext = null;
|
|
870
1108
|
// Keep host-provided option closures (notably chat delegation) aligned with
|
|
871
1109
|
// the live selection after an in-session model switch.
|
|
872
1110
|
this.opts.provider = this.provider;
|
|
@@ -944,6 +1182,7 @@ export class AgentSession {
|
|
|
944
1182
|
}
|
|
945
1183
|
}
|
|
946
1184
|
async compact(existingCredentials) {
|
|
1185
|
+
this.lastCompactionCompacted = false;
|
|
947
1186
|
const creds = existingCredentials ??
|
|
948
1187
|
(await this.authStorage.resolveCredentials(this.provider, {
|
|
949
1188
|
storageKeys: this.currentAuthStorageKeys(),
|
|
@@ -964,6 +1203,15 @@ export class AgentSession {
|
|
|
964
1203
|
signal: this.opts.signal,
|
|
965
1204
|
});
|
|
966
1205
|
this.messages = result.messages;
|
|
1206
|
+
this.lastCompactionCompacted = result.result.compacted;
|
|
1207
|
+
if (!result.result.compacted) {
|
|
1208
|
+
this.eventBus.emit("compaction_end", {
|
|
1209
|
+
originalCount: result.result.originalCount,
|
|
1210
|
+
newCount: result.result.newCount,
|
|
1211
|
+
});
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
this.providerContext = null;
|
|
967
1215
|
// Transient sessions (Nolan chat/autopilot, subagent spawns) must NEVER touch
|
|
968
1216
|
// the session store: without this guard, the first auto-compaction called
|
|
969
1217
|
// sessionManager.create() and assigned a real sessionPath, silently turning
|
|
@@ -976,9 +1224,14 @@ export class AgentSession {
|
|
|
976
1224
|
else {
|
|
977
1225
|
// Persist compacted messages to a new session file so `ezcoder continue`
|
|
978
1226
|
// picks up the compacted state instead of the full original history.
|
|
979
|
-
const session = await this.sessionManager.create(this.cwd, this.provider, this.model
|
|
1227
|
+
const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
|
|
1228
|
+
conversationId: this.conversationId || undefined,
|
|
1229
|
+
preview: this.sessionPreview || undefined,
|
|
1230
|
+
});
|
|
980
1231
|
this.sessionId = session.id;
|
|
1232
|
+
this.conversationId = session.header.conversationId ?? session.id;
|
|
981
1233
|
this.sessionPath = session.path;
|
|
1234
|
+
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
982
1235
|
// Write compacted messages (skip system — it's rebuilt on load)
|
|
983
1236
|
for (const msg of this.messages) {
|
|
984
1237
|
if (msg.role === "system")
|
|
@@ -986,7 +1239,8 @@ export class AgentSession {
|
|
|
986
1239
|
await this.persistMessage(msg);
|
|
987
1240
|
}
|
|
988
1241
|
this.lastPersistedIndex = this.messages.length;
|
|
989
|
-
// Carry Nolan's advisory turns into the new file so they survive compaction.
|
|
1242
|
+
// Carry evidence and Nolan's advisory turns into the new file so they survive compaction.
|
|
1243
|
+
await this.rePersistTurnMetrics();
|
|
990
1244
|
await this.rePersistNolanTurns();
|
|
991
1245
|
await this.rePersistAutopilotMarkers();
|
|
992
1246
|
await this.rePersistAppMarkers();
|
|
@@ -1002,7 +1256,13 @@ export class AgentSession {
|
|
|
1002
1256
|
newCount: result.result.newCount,
|
|
1003
1257
|
});
|
|
1004
1258
|
}
|
|
1005
|
-
async newSession() {
|
|
1259
|
+
async newSession(preserveConversation = false) {
|
|
1260
|
+
// Approved-plan execution is a clean checkpoint of the same conversation;
|
|
1261
|
+
// explicit new sessions reset the conversation identity.
|
|
1262
|
+
if (!preserveConversation) {
|
|
1263
|
+
this.conversationId = "";
|
|
1264
|
+
this.sessionPreview = "";
|
|
1265
|
+
}
|
|
1006
1266
|
// A fresh session drops any in-flight plan state so its prompt is clean.
|
|
1007
1267
|
this.planModeRef.current = false;
|
|
1008
1268
|
this.approvedPlanPath = undefined;
|
|
@@ -1013,6 +1273,7 @@ export class AgentSession {
|
|
|
1013
1273
|
this.nolanTurns = [];
|
|
1014
1274
|
this.autopilotMarkers = [];
|
|
1015
1275
|
this.appMarkers = [];
|
|
1276
|
+
this.turnMetrics = [];
|
|
1016
1277
|
const basePrompt = this.customSystemPrompt ??
|
|
1017
1278
|
(await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider));
|
|
1018
1279
|
this.baseSystemPrompt = basePrompt;
|
|
@@ -1027,16 +1288,21 @@ export class AgentSession {
|
|
|
1027
1288
|
// polluted the project's session list.
|
|
1028
1289
|
if (this.opts.transient) {
|
|
1029
1290
|
this.sessionId = "";
|
|
1291
|
+
this.conversationId = "";
|
|
1292
|
+
this.sessionPreview = "";
|
|
1030
1293
|
this.sessionPath = "";
|
|
1031
1294
|
this.lastPersistedIndex = this.messages.length;
|
|
1032
1295
|
}
|
|
1033
1296
|
else {
|
|
1034
1297
|
await this.createNewSession();
|
|
1298
|
+
await this.subAgentManager?.resetParentSession(this.sessionId);
|
|
1035
1299
|
}
|
|
1036
1300
|
this.eventBus.emit("session_start", { sessionId: this.sessionId });
|
|
1037
1301
|
}
|
|
1038
1302
|
async loadSession(sessionPath) {
|
|
1039
1303
|
await this.loadExistingSession(sessionPath);
|
|
1304
|
+
if (this.sessionId)
|
|
1305
|
+
await this.subAgentManager?.hydrate(this.sessionId);
|
|
1040
1306
|
this.eventBus.emit("session_start", { sessionId: this.sessionId });
|
|
1041
1307
|
}
|
|
1042
1308
|
/**
|
|
@@ -1096,6 +1362,18 @@ export class AgentSession {
|
|
|
1096
1362
|
getPlanMode() {
|
|
1097
1363
|
return this.planModeRef.current;
|
|
1098
1364
|
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Suppress only the pre-final Ideal self-review for this live session.
|
|
1367
|
+
* Autopilot uses this while Nolan independently owns verification; loop-break
|
|
1368
|
+
* and post-compaction re-grounding remain active.
|
|
1369
|
+
*/
|
|
1370
|
+
setIdealReviewSuppressed(suppressed) {
|
|
1371
|
+
this.idealReviewSuppressed = suppressed;
|
|
1372
|
+
if (suppressed) {
|
|
1373
|
+
this.idealReviewPhase = "idle";
|
|
1374
|
+
this.reviewCoverage.reset();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1099
1377
|
/** Queue a user message (optionally with attachments) to be injected mid-run
|
|
1100
1378
|
* as steering. Returns the new queue length. No-op semantics are the caller's
|
|
1101
1379
|
* concern. */
|
|
@@ -1206,6 +1484,39 @@ export class AgentSession {
|
|
|
1206
1484
|
getMessages() {
|
|
1207
1485
|
return this.messages;
|
|
1208
1486
|
}
|
|
1487
|
+
getTurnMetrics() {
|
|
1488
|
+
return this.turnMetrics.map((metric) => ({
|
|
1489
|
+
...metric,
|
|
1490
|
+
usage: { ...metric.usage },
|
|
1491
|
+
timing: { ...metric.timing },
|
|
1492
|
+
cost: { ...metric.cost },
|
|
1493
|
+
}));
|
|
1494
|
+
}
|
|
1495
|
+
async persistTurnMetric(event) {
|
|
1496
|
+
const payload = {
|
|
1497
|
+
version: 1,
|
|
1498
|
+
turn: event.turn,
|
|
1499
|
+
provider: this.provider,
|
|
1500
|
+
model: this.model,
|
|
1501
|
+
stopReason: event.stopReason,
|
|
1502
|
+
usage: { ...event.usage },
|
|
1503
|
+
timing: { ...event.timing },
|
|
1504
|
+
cost: {
|
|
1505
|
+
status: "unavailable",
|
|
1506
|
+
reason: "No authoritative effective-dated provider pricing is available",
|
|
1507
|
+
},
|
|
1508
|
+
};
|
|
1509
|
+
this.turnMetrics.push(payload);
|
|
1510
|
+
if (this.sessionPath)
|
|
1511
|
+
await this.sessionManager.appendTurnMetric(this.sessionPath, payload);
|
|
1512
|
+
}
|
|
1513
|
+
async rePersistTurnMetrics() {
|
|
1514
|
+
if (!this.sessionPath)
|
|
1515
|
+
return;
|
|
1516
|
+
for (const metric of this.turnMetrics) {
|
|
1517
|
+
await this.sessionManager.appendTurnMetric(this.sessionPath, metric);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1209
1520
|
/** Nolan Grout (mentor) turns recorded against this session, in record order. Used
|
|
1210
1521
|
* by the host to interleave Nolan's advisory exchanges back into the transcript
|
|
1211
1522
|
* on resume. Never part of the LLM message history. */
|
|
@@ -1360,46 +1671,12 @@ export class AgentSession {
|
|
|
1360
1671
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1361
1672
|
}
|
|
1362
1673
|
}
|
|
1363
|
-
/**
|
|
1364
|
-
* Generate a short LLM session title from the conversation so far (first user
|
|
1365
|
-
* message + first assistant reply). Best-effort; returns null on failure or
|
|
1366
|
-
* when there's no user message yet. Uses the cheapest model for the provider.
|
|
1367
|
-
*/
|
|
1368
|
-
async generateTitle() {
|
|
1369
|
-
const extractText = (content) => typeof content === "string"
|
|
1370
|
-
? content
|
|
1371
|
-
: content
|
|
1372
|
-
.map((c) => c.type === "text" && "text" in c && typeof c.text === "string" ? c.text : "")
|
|
1373
|
-
.join(" ");
|
|
1374
|
-
const userMsg = this.messages.find((m) => m.role === "user");
|
|
1375
|
-
const assistantMsg = this.messages.find((m) => m.role === "assistant");
|
|
1376
|
-
const userText = userMsg ? extractText(userMsg.content) : "";
|
|
1377
|
-
if (!userText.trim())
|
|
1378
|
-
return null;
|
|
1379
|
-
try {
|
|
1380
|
-
const creds = await this.authStorage.resolveCredentials(this.provider, {
|
|
1381
|
-
storageKeys: this.currentAuthStorageKeys(),
|
|
1382
|
-
});
|
|
1383
|
-
const title = await generateSessionTitle({
|
|
1384
|
-
provider: this.provider,
|
|
1385
|
-
userMessage: userText,
|
|
1386
|
-
assistantPreview: assistantMsg ? extractText(assistantMsg.content).slice(0, 200) : "",
|
|
1387
|
-
apiKey: creds.accessToken,
|
|
1388
|
-
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
1389
|
-
accountId: creds.accountId,
|
|
1390
|
-
});
|
|
1391
|
-
return title || null;
|
|
1392
|
-
}
|
|
1393
|
-
catch {
|
|
1394
|
-
return null;
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
1674
|
/**
|
|
1398
1675
|
* Rewrite a draft prompt into a tighter, terminology-correct version using
|
|
1399
1676
|
* the ACTIVE provider/model. A stateless one-off LLM call (no agent loop, no
|
|
1400
1677
|
* tools, no session mutation) — safe to run even mid-run. Returns the plain
|
|
1401
1678
|
* enhanced text plus typed segments marking each corrected term. Errors throw
|
|
1402
|
-
* so the caller can surface them
|
|
1679
|
+
* so the caller can surface them.
|
|
1403
1680
|
*/
|
|
1404
1681
|
async enhancePrompt(text) {
|
|
1405
1682
|
if (!text.trim())
|
|
@@ -1497,8 +1774,12 @@ export class AgentSession {
|
|
|
1497
1774
|
}
|
|
1498
1775
|
// ── Private ────────────────────────────────────────────
|
|
1499
1776
|
async createNewSession() {
|
|
1500
|
-
const session = await this.sessionManager.create(this.cwd, this.provider, this.model
|
|
1777
|
+
const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
|
|
1778
|
+
conversationId: this.conversationId || undefined,
|
|
1779
|
+
preview: this.sessionPreview || undefined,
|
|
1780
|
+
});
|
|
1501
1781
|
this.sessionId = session.id;
|
|
1782
|
+
this.conversationId = session.header.conversationId ?? session.id;
|
|
1502
1783
|
this.sessionPath = session.path;
|
|
1503
1784
|
this.lastPersistedIndex = this.messages.length;
|
|
1504
1785
|
}
|
|
@@ -1506,6 +1787,15 @@ export class AgentSession {
|
|
|
1506
1787
|
const loaded = await this.sessionManager.load(sessionPath);
|
|
1507
1788
|
// Use the leaf from the header to walk the correct branch
|
|
1508
1789
|
const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
|
|
1790
|
+
this.conversationId = loaded.header.conversationId ?? loaded.header.id;
|
|
1791
|
+
const legacyLabel = [...loaded.entries]
|
|
1792
|
+
.reverse()
|
|
1793
|
+
.find((entry) => entry.type === "label")
|
|
1794
|
+
?.label.replace(/\s+/g, " ")
|
|
1795
|
+
.trim()
|
|
1796
|
+
.slice(0, 80);
|
|
1797
|
+
this.sessionPreview =
|
|
1798
|
+
legacyLabel || loaded.header.preview || findUserSessionPrompt(loadedMessages);
|
|
1509
1799
|
// Restore Nolan's advisory turns (custom entries, not on the message branch) so
|
|
1510
1800
|
// they reappear in the transcript and survive into the continuation file.
|
|
1511
1801
|
this.nolanTurns = this.sessionManager.getNolanTurns(loaded.entries);
|
|
@@ -1513,6 +1803,7 @@ export class AgentSession {
|
|
|
1513
1803
|
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries);
|
|
1514
1804
|
// Restore app transcript markers (plan banner / task header / errors / hints).
|
|
1515
1805
|
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries);
|
|
1806
|
+
this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
|
|
1516
1807
|
// Track the current leaf for subsequent entries
|
|
1517
1808
|
this.currentLeafId = loaded.header.leafId;
|
|
1518
1809
|
// Rebuild messages: keep system, add loaded
|
|
@@ -1531,7 +1822,16 @@ export class AgentSession {
|
|
|
1531
1822
|
provider: this.provider,
|
|
1532
1823
|
accountId: creds.accountId,
|
|
1533
1824
|
});
|
|
1534
|
-
|
|
1825
|
+
const needsLoadCompaction = this.settingsManager.get("autoCompact") &&
|
|
1826
|
+
shouldCompact(this.messages, contextWindow, this.settingsManager.get("compactThreshold"));
|
|
1827
|
+
if (needsLoadCompaction && this.opts.deferLoadCompaction) {
|
|
1828
|
+
// Host readiness is gated on initialize() — don't block it on a summary
|
|
1829
|
+
// LLM call (up to 30s). runLoop()'s pre-run auto-compaction picks this
|
|
1830
|
+
// up on the first prompt and emits compaction_start/_end for the UI.
|
|
1831
|
+
log("INFO", "session", "Restored session exceeds context — deferring compaction to first prompt");
|
|
1832
|
+
}
|
|
1833
|
+
else if (needsLoadCompaction) {
|
|
1834
|
+
await this.subAgentManager?.hydrate(loaded.header.id);
|
|
1535
1835
|
log("INFO", "session", `Restored session exceeds context — auto-compacting`);
|
|
1536
1836
|
const compacted = await compact(this.messages, {
|
|
1537
1837
|
provider: this.provider,
|
|
@@ -1552,9 +1852,14 @@ export class AgentSession {
|
|
|
1552
1852
|
// what's in memory — fork a fresh session file for the compacted state
|
|
1553
1853
|
// (mirrors compact()'s own persistence) so `ezcoder continue` picks up
|
|
1554
1854
|
// the summary instead of the full original transcript.
|
|
1555
|
-
const session = await this.sessionManager.create(this.cwd, this.provider, this.model
|
|
1855
|
+
const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
|
|
1856
|
+
conversationId: this.conversationId || undefined,
|
|
1857
|
+
preview: this.sessionPreview || undefined,
|
|
1858
|
+
});
|
|
1556
1859
|
this.sessionId = session.id;
|
|
1860
|
+
this.conversationId = session.header.conversationId ?? session.id;
|
|
1557
1861
|
this.sessionPath = session.path;
|
|
1862
|
+
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
1558
1863
|
this.currentLeafId = null;
|
|
1559
1864
|
// Re-persist (compacted) messages — skip system, it's rebuilt on load
|
|
1560
1865
|
for (const msg of this.messages) {
|
|
@@ -1563,7 +1868,8 @@ export class AgentSession {
|
|
|
1563
1868
|
await this.persistMessage(msg);
|
|
1564
1869
|
}
|
|
1565
1870
|
this.lastPersistedIndex = this.messages.length;
|
|
1566
|
-
// Carry Nolan
|
|
1871
|
+
// Carry restored evidence and Nolan turns into the continuation file.
|
|
1872
|
+
await this.rePersistTurnMetrics();
|
|
1567
1873
|
await this.rePersistNolanTurns();
|
|
1568
1874
|
await this.rePersistAutopilotMarkers();
|
|
1569
1875
|
await this.rePersistAppMarkers();
|
|
@@ -1587,6 +1893,9 @@ export class AgentSession {
|
|
|
1587
1893
|
return this.messages;
|
|
1588
1894
|
}
|
|
1589
1895
|
async persistMessage(message) {
|
|
1896
|
+
if (!this.sessionPreview && message.role === "user") {
|
|
1897
|
+
this.sessionPreview = getUserSessionPrompt(message.content) ?? "";
|
|
1898
|
+
}
|
|
1590
1899
|
// Transient sessions (subagent spawns) have no session file — skip.
|
|
1591
1900
|
if (!this.sessionPath)
|
|
1592
1901
|
return;
|