@kenkaiiii/ggcoder 5.67.1 → 5.68.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 +121 -3
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/agent-session.d.ts +50 -3
- package/dist/core/agent-session.js +344 -21
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/ask-user.d.ts +90 -8
- package/dist/core/ask-user.js +124 -13
- package/dist/core/ask-user.js.map +1 -1
- package/dist/core/cache-diagnostics.d.ts +62 -0
- package/dist/core/cache-diagnostics.js +165 -0
- package/dist/core/cache-diagnostics.js.map +1 -0
- package/dist/core/compaction/plan-step-policy.d.ts +46 -0
- package/dist/core/compaction/plan-step-policy.js +57 -0
- package/dist/core/compaction/plan-step-policy.js.map +1 -0
- package/dist/core/destructive-git-guard.d.ts +63 -0
- package/dist/core/destructive-git-guard.js +831 -0
- package/dist/core/destructive-git-guard.js.map +1 -0
- package/dist/core/event-bus.d.ts +3 -0
- package/dist/core/event-bus.js +5 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/keep-awake.d.ts +88 -0
- package/dist/core/keep-awake.js +251 -0
- package/dist/core/keep-awake.js.map +1 -0
- package/dist/core/package-threats.d.ts +18 -0
- package/dist/core/package-threats.js +168 -0
- package/dist/core/package-threats.js.map +1 -0
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.js +38 -18
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-summary-index.d.ts +37 -0
- package/dist/core/session-summary-index.js +172 -0
- package/dist/core/session-summary-index.js.map +1 -0
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/shell-threats-popular-packages.d.ts +11 -0
- package/dist/core/shell-threats-popular-packages.js +675 -0
- package/dist/core/shell-threats-popular-packages.js.map +1 -0
- package/dist/core/shell-threats.d.ts +8 -0
- package/dist/core/shell-threats.js +186 -0
- package/dist/core/shell-threats.js.map +1 -0
- package/dist/core/stream-rules.d.ts +30 -0
- package/dist/core/stream-rules.js +151 -0
- package/dist/core/stream-rules.js.map +1 -0
- package/dist/motion-agent/motion-check-tool.d.ts +7 -0
- package/dist/motion-agent/motion-check-tool.js +80 -30
- package/dist/motion-agent/motion-check-tool.js.map +1 -1
- package/dist/motion-agent/motion-review.d.ts +5 -2
- package/dist/motion-agent/motion-review.js +7 -4
- package/dist/motion-agent/motion-review.js.map +1 -1
- package/dist/tools/ask-user.js +3 -3
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/bash.js +37 -0
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/task-send.d.ts +1 -1
- package/dist/tools/task-send.js +15 -1
- package/dist/tools/task-send.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +1 -0
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +1 -1
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agentLoop, isAbortError, isUsageLimitError, } from "@kenkaiiii/gg-agent";
|
|
1
|
+
import { agentLoop, isAbortError, isUsageLimitError, repairToolPairingAdjacent, } from "@kenkaiiii/gg-agent";
|
|
2
2
|
import { ProviderError, stream, } from "@kenkaiiii/gg-ai";
|
|
3
3
|
import { EventBus } from "./event-bus.js";
|
|
4
4
|
import { COMPLETION_REVIEW_STATE_KIND, } from "./completion-review.js";
|
|
@@ -36,9 +36,13 @@ import { createToolSearchTool } from "../tools/tool-search.js";
|
|
|
36
36
|
import { createSessionStatsTool } from "../tools/session-stats.js";
|
|
37
37
|
import { createDiagnoseCommand, isInternalDiagnosticsEnabled, SessionDiagnosticsRecorder, } from "./internal-diagnostics.js";
|
|
38
38
|
import { log } from "./logger.js";
|
|
39
|
-
import {
|
|
39
|
+
import { CacheDiagnostics } from "./cache-diagnostics.js";
|
|
40
|
+
import { setEstimatorModel, calibrateEstimatorFromUsage, estimateConversationTokens, } from "./compaction/token-estimator.js";
|
|
40
41
|
import { calculateActiveContextTokens } from "./compaction/active-context.js";
|
|
41
42
|
import { resolveCompactionPolicy } from "./compaction/policy.js";
|
|
43
|
+
import { decidePlanStepCompaction, DEFAULT_CACHE_WRITE_READ_RATIO, PLAN_STEP_KEEP_TOKENS, } from "./compaction/plan-step-policy.js";
|
|
44
|
+
import { extractPlanSteps, findCompletedMarkers } from "../utils/plan-steps.js";
|
|
45
|
+
import { readFileSync } from "node:fs";
|
|
42
46
|
import { clampThinkingForPlanMode } from "./thinking-level.js";
|
|
43
47
|
import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
44
48
|
import { discoverAgents } from "./agents.js";
|
|
@@ -57,6 +61,7 @@ import { classifyVerificationCommand } from "./verification-evidence.js";
|
|
|
57
61
|
import { captureVerificationSnapshot } from "./verification-snapshot.js";
|
|
58
62
|
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
59
63
|
import { normalizeMessageImages } from "./message-images.js";
|
|
64
|
+
import { loadStreamRules } from "./stream-rules.js";
|
|
60
65
|
import crypto from "node:crypto";
|
|
61
66
|
import fs from "node:fs/promises";
|
|
62
67
|
import os from "node:os";
|
|
@@ -130,6 +135,7 @@ export class AgentSession {
|
|
|
130
135
|
// transcript rows the live run showed.
|
|
131
136
|
appMarkers = [];
|
|
132
137
|
turnMetrics = [];
|
|
138
|
+
cacheDiagnostics = new CacheDiagnostics();
|
|
133
139
|
/** Internal-only (GG_INTERNAL): live per-session cost/reliability recorder.
|
|
134
140
|
* Absent entirely in public builds — see core/internal-diagnostics.ts. */
|
|
135
141
|
diagnosticsRecorder;
|
|
@@ -216,6 +222,25 @@ export class AgentSession {
|
|
|
216
222
|
postTurnCompaction;
|
|
217
223
|
lastCompactionCompacted = false;
|
|
218
224
|
compactionRetryAfter = 0;
|
|
225
|
+
/**
|
|
226
|
+
* SoL-Pi plan-step compaction bookkeeping (see compaction/plan-step-policy.ts).
|
|
227
|
+
* Step progress comes from `[DONE:n]` markers in assistant text — the same
|
|
228
|
+
* contract the approved-plan UI tracks.
|
|
229
|
+
*/
|
|
230
|
+
planStepState = {
|
|
231
|
+
planPath: undefined,
|
|
232
|
+
scanIndex: 0,
|
|
233
|
+
doneSteps: new Set(),
|
|
234
|
+
requestsInCompletedSteps: 0,
|
|
235
|
+
requestsThisStep: 0,
|
|
236
|
+
requests: 0,
|
|
237
|
+
grownTokens: 0,
|
|
238
|
+
lastContextTokens: 0,
|
|
239
|
+
compactions: 0,
|
|
240
|
+
writeCostBalance: 0,
|
|
241
|
+
savingPerRequest: 0,
|
|
242
|
+
requestsSinceLastCompaction: 0,
|
|
243
|
+
};
|
|
219
244
|
/** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
|
|
220
245
|
deferredCompactionPending = false;
|
|
221
246
|
/** Latest provider count, anchored to the assistant response it measured. */
|
|
@@ -923,6 +948,7 @@ export class AgentSession {
|
|
|
923
948
|
kind: "prompt",
|
|
924
949
|
visibility: "transcript",
|
|
925
950
|
}, options = {}) {
|
|
951
|
+
this.prewarmController?.abort();
|
|
926
952
|
await this.settlePostTurnCompaction();
|
|
927
953
|
await this.adoptDeferredCheckpointBeforePrompt();
|
|
928
954
|
const slash = await this.resolveSlashInput(content);
|
|
@@ -958,6 +984,7 @@ export class AgentSession {
|
|
|
958
984
|
* attachments are always a direct conversational turn.
|
|
959
985
|
*/
|
|
960
986
|
async promptWithAttachments(text, attachments) {
|
|
987
|
+
this.prewarmController?.abort();
|
|
961
988
|
await this.settlePostTurnCompaction();
|
|
962
989
|
await this.adoptDeferredCheckpointBeforePrompt();
|
|
963
990
|
const parts = this.buildAttachmentParts(text, attachments);
|
|
@@ -2035,14 +2062,22 @@ export class AgentSession {
|
|
|
2035
2062
|
content: `${String(message.content)}\n\n${notes.join(" ")}`,
|
|
2036
2063
|
};
|
|
2037
2064
|
}
|
|
2038
|
-
/**
|
|
2065
|
+
/** Wraps the real run: cancels an in-flight cache prewarm and tracks run
|
|
2066
|
+
* activity / last real request time for {@link prewarm}. */
|
|
2039
2067
|
async runLoop(options = {}) {
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2068
|
+
this.prewarmController?.abort();
|
|
2069
|
+
this.runLoopDepth++;
|
|
2070
|
+
try {
|
|
2071
|
+
await this.runLoopInner(options);
|
|
2072
|
+
}
|
|
2073
|
+
finally {
|
|
2074
|
+
this.runLoopDepth--;
|
|
2075
|
+
this.lastRealRequestAt = Date.now();
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
|
|
2079
|
+
async runLoopInner(options = {}) {
|
|
2080
|
+
await this.prepareSystemPromptForRequest();
|
|
2046
2081
|
// One-shot cache-key marker per session so turn_end cacheRead numbers
|
|
2047
2082
|
// in the log can be traced back to a specific routing namespace —
|
|
2048
2083
|
// particularly useful when sub-agents inherit `parentKey:subagent`.
|
|
@@ -2137,11 +2172,14 @@ export class AgentSession {
|
|
|
2137
2172
|
}
|
|
2138
2173
|
const userAgent = this.provider === "anthropic" ? await getClaudeCliUserAgent() : undefined;
|
|
2139
2174
|
const loopMessages = await this.prepareDynamicContext();
|
|
2175
|
+
// Re-read every run so rule-file edits apply on the next turn; a few small files.
|
|
2176
|
+
const streamRules = await loadStreamRules(this.cwd);
|
|
2140
2177
|
const runAgentLoop = async (apiKey, accountId, projectId) => {
|
|
2141
2178
|
lastResolvedAccessToken = apiKey;
|
|
2142
2179
|
const modelInfo = getModel(this.model);
|
|
2143
2180
|
const effectiveBaseUrl = this.baseUrl ?? creds.baseUrl;
|
|
2144
2181
|
const generator = agentLoop(loopMessages, {
|
|
2182
|
+
...(streamRules.length > 0 ? { streamRules: { rules: streamRules } } : {}),
|
|
2145
2183
|
provider: this.provider,
|
|
2146
2184
|
model: this.model,
|
|
2147
2185
|
tools: options.disableTools ? [] : this.tools,
|
|
@@ -2189,6 +2227,33 @@ export class AgentSession {
|
|
|
2189
2227
|
// + pre-warm before the first turn. "baseline": current 5-min default.
|
|
2190
2228
|
cacheRetention: this.isSpeedOptimized() ? "long" : "short",
|
|
2191
2229
|
promptCacheKey: this.getPromptCacheKey(),
|
|
2230
|
+
onContextPrepared: (context) => {
|
|
2231
|
+
const report = this.cacheDiagnostics.prepare(context, {
|
|
2232
|
+
provider: this.provider,
|
|
2233
|
+
model: this.model,
|
|
2234
|
+
at: Date.now(),
|
|
2235
|
+
cacheRetention: this.isSpeedOptimized() ? "long" : "short",
|
|
2236
|
+
route: { baseUrl: effectiveBaseUrl, accountId: this.lastAccountId ?? accountId },
|
|
2237
|
+
settings: {
|
|
2238
|
+
thinking: this.planModeRef.current || options.capThinking
|
|
2239
|
+
? clampThinkingForPlanMode(this.thinkingLevel)
|
|
2240
|
+
: this.thinkingLevel,
|
|
2241
|
+
webSearch: !options.disableTools,
|
|
2242
|
+
supportsImages: modelInfo?.supportsImages,
|
|
2243
|
+
promptCacheKey: this.getPromptCacheKey(),
|
|
2244
|
+
},
|
|
2245
|
+
});
|
|
2246
|
+
log("INFO", "cache", "Prepared context", {
|
|
2247
|
+
sessionId: this.sessionId || this.transportSessionId,
|
|
2248
|
+
data: JSON.stringify(report),
|
|
2249
|
+
});
|
|
2250
|
+
if (report.thinkingPrefixRiskBlocks > 0) {
|
|
2251
|
+
log("WARN", "cache", "Possible signed-thinking prefix mismatch; not server verified", {
|
|
2252
|
+
sessionId: this.sessionId || this.transportSessionId,
|
|
2253
|
+
blocks: String(report.thinkingPrefixRiskBlocks),
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
},
|
|
2192
2257
|
supportsImages: modelInfo?.supportsImages,
|
|
2193
2258
|
supportsVideo: modelInfo?.supportsVideo,
|
|
2194
2259
|
userAgent,
|
|
@@ -2225,6 +2290,7 @@ export class AgentSession {
|
|
|
2225
2290
|
// retained usage afterwards since it counted the pruned content.
|
|
2226
2291
|
const pruneResult = pruneStaleToolResults(messages);
|
|
2227
2292
|
if (pruneResult.pruned) {
|
|
2293
|
+
this.cacheDiagnostics.noteEdit("tool_prune", pruneResult.freedTokens);
|
|
2228
2294
|
this.providerContext = null;
|
|
2229
2295
|
log("INFO", "compaction", "Pruned stale tool outputs", {
|
|
2230
2296
|
prunedResults: String(pruneResult.prunedResults),
|
|
@@ -2265,6 +2331,9 @@ export class AgentSession {
|
|
|
2265
2331
|
usage,
|
|
2266
2332
|
pendingMessages,
|
|
2267
2333
|
});
|
|
2334
|
+
// An approved plan runs as ONE run, so step boundaries are only
|
|
2335
|
+
// visible here, between model steps — not after the run ends.
|
|
2336
|
+
const planStep = this.observePlanStepProgress(messages, contextWindow, activeTokens);
|
|
2268
2337
|
log("INFO", "compaction", "In-flight compaction decision", {
|
|
2269
2338
|
provider: this.provider,
|
|
2270
2339
|
model: this.model,
|
|
@@ -2272,8 +2341,10 @@ export class AgentSession {
|
|
|
2272
2341
|
contextWindow: String(contextWindow),
|
|
2273
2342
|
activeTokens: String(activeTokens),
|
|
2274
2343
|
triggerLimit: String(policy.targetTokens),
|
|
2344
|
+
...(planStep ? { planStep: `${planStep.compact} (${planStep.reason})` } : {}),
|
|
2275
2345
|
});
|
|
2276
|
-
if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens, policy.targetTokens)
|
|
2346
|
+
if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens, policy.targetTokens) &&
|
|
2347
|
+
!planStep?.compact)
|
|
2277
2348
|
return messages;
|
|
2278
2349
|
}
|
|
2279
2350
|
// compact() operates on this.messages, while an earlier transform may
|
|
@@ -2594,7 +2665,7 @@ export class AgentSession {
|
|
|
2594
2665
|
const canonicalPath = await this.sessionManager.resolveCanonicalSession(this.conversationId, this.cwd);
|
|
2595
2666
|
if (!canonicalPath || canonicalPath === this.sessionPath)
|
|
2596
2667
|
return;
|
|
2597
|
-
await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath));
|
|
2668
|
+
await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath, { canonical: true }));
|
|
2598
2669
|
}
|
|
2599
2670
|
async persistCompactionCheckpoint(sourceFingerprint, result) {
|
|
2600
2671
|
const parentSessionId = this.sessionId || undefined;
|
|
@@ -2643,6 +2714,99 @@ export class AgentSession {
|
|
|
2643
2714
|
if (this.postTurnCompaction)
|
|
2644
2715
|
await this.postTurnCompaction;
|
|
2645
2716
|
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Advance plan-step bookkeeping over the messages added since the last
|
|
2719
|
+
* observation and, when a plan step was newly completed (`[DONE:n]`), run
|
|
2720
|
+
* the SoL-Pi cost rule. Called between model steps (in-flight) and once
|
|
2721
|
+
* after the run; `scanIndex` and `doneSteps` make each message and each
|
|
2722
|
+
* step count once across both paths. Returns undefined when no step
|
|
2723
|
+
* completed since the last observation.
|
|
2724
|
+
*/
|
|
2725
|
+
observePlanStepProgress(messages, contextWindow, activeTokens) {
|
|
2726
|
+
const st = this.planStepState;
|
|
2727
|
+
const planPath = this.approvedPlanPath;
|
|
2728
|
+
if (st.planPath !== planPath) {
|
|
2729
|
+
st.planPath = planPath;
|
|
2730
|
+
st.doneSteps = new Set();
|
|
2731
|
+
st.requestsInCompletedSteps = 0;
|
|
2732
|
+
st.requestsThisStep = 0;
|
|
2733
|
+
}
|
|
2734
|
+
if (st.scanIndex > messages.length)
|
|
2735
|
+
st.scanIndex = 0;
|
|
2736
|
+
let requests = 0;
|
|
2737
|
+
const newlyDone = [];
|
|
2738
|
+
for (let i = st.scanIndex; i < messages.length; i++) {
|
|
2739
|
+
const msg = messages[i];
|
|
2740
|
+
if (msg?.role !== "assistant")
|
|
2741
|
+
continue;
|
|
2742
|
+
requests++;
|
|
2743
|
+
const text = typeof msg.content === "string"
|
|
2744
|
+
? msg.content
|
|
2745
|
+
: msg.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
|
|
2746
|
+
for (const step of findCompletedMarkers(text)) {
|
|
2747
|
+
if (!st.doneSteps.has(step))
|
|
2748
|
+
newlyDone.push(step);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
st.scanIndex = messages.length;
|
|
2752
|
+
const contextTokens = activeTokens ?? estimateConversationTokens(messages);
|
|
2753
|
+
if (st.lastContextTokens > 0)
|
|
2754
|
+
st.grownTokens += Math.max(0, contextTokens - st.lastContextTokens);
|
|
2755
|
+
st.lastContextTokens = contextTokens;
|
|
2756
|
+
st.requests += requests;
|
|
2757
|
+
st.requestsThisStep += requests;
|
|
2758
|
+
st.requestsSinceLastCompaction += requests;
|
|
2759
|
+
if (st.writeCostBalance > 0)
|
|
2760
|
+
st.writeCostBalance -= st.savingPerRequest * requests;
|
|
2761
|
+
if (!planPath || newlyDone.length === 0)
|
|
2762
|
+
return undefined;
|
|
2763
|
+
for (const step of newlyDone)
|
|
2764
|
+
st.doneSteps.add(step);
|
|
2765
|
+
st.requestsInCompletedSteps += st.requestsThisStep;
|
|
2766
|
+
st.requestsThisStep = 0;
|
|
2767
|
+
let planText;
|
|
2768
|
+
try {
|
|
2769
|
+
planText = readFileSync(planPath, "utf8");
|
|
2770
|
+
}
|
|
2771
|
+
catch {
|
|
2772
|
+
return undefined;
|
|
2773
|
+
}
|
|
2774
|
+
const totalSteps = extractPlanSteps(planText).length;
|
|
2775
|
+
const decision = decidePlanStepCompaction({
|
|
2776
|
+
contextTokens,
|
|
2777
|
+
keptTailTokens: PLAN_STEP_KEEP_TOKENS,
|
|
2778
|
+
contextWindow,
|
|
2779
|
+
// The model registry carries no cache price fields; use the SoL-Pi default.
|
|
2780
|
+
cacheWriteReadRatio: DEFAULT_CACHE_WRITE_READ_RATIO,
|
|
2781
|
+
stepsCompleted: st.doneSteps.size,
|
|
2782
|
+
stepsRemaining: Math.max(0, totalSteps - st.doneSteps.size),
|
|
2783
|
+
requestsInCompletedSteps: st.requestsInCompletedSteps,
|
|
2784
|
+
requests: st.requests,
|
|
2785
|
+
grownTokens: st.grownTokens,
|
|
2786
|
+
priorCompactions: st.compactions,
|
|
2787
|
+
writeCostBalance: st.writeCostBalance,
|
|
2788
|
+
requestsSinceLastCompaction: st.compactions > 0 ? st.requestsSinceLastCompaction : undefined,
|
|
2789
|
+
});
|
|
2790
|
+
log("INFO", "compaction", "Plan-step compaction decision", {
|
|
2791
|
+
compact: String(decision.compact),
|
|
2792
|
+
reason: decision.reason,
|
|
2793
|
+
});
|
|
2794
|
+
return decision;
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Plan-step bookkeeping after ANY successful compaction (as in the bench:
|
|
2798
|
+
* every compaction leaves a cache-write cost to be repaid by later savings).
|
|
2799
|
+
*/
|
|
2800
|
+
recordPlanStepCompaction(contextBefore) {
|
|
2801
|
+
const st = this.planStepState;
|
|
2802
|
+
const after = estimateConversationTokens(this.messages);
|
|
2803
|
+
st.scanIndex = this.messages.length;
|
|
2804
|
+
st.lastContextTokens = after;
|
|
2805
|
+
st.requestsSinceLastCompaction = 0;
|
|
2806
|
+
st.compactions++;
|
|
2807
|
+
st.writeCostBalance += after * (DEFAULT_CACHE_WRITE_READ_RATIO - 1);
|
|
2808
|
+
st.savingPerRequest = Math.max(0, contextBefore - after);
|
|
2809
|
+
}
|
|
2646
2810
|
/**
|
|
2647
2811
|
* Post-turn compaction: once the final response has been delivered, compact
|
|
2648
2812
|
* in the background while the user reads the answer, instead of making the
|
|
@@ -2651,7 +2815,9 @@ export class AgentSession {
|
|
|
2651
2815
|
* Codex `model_post_turn_compact_threshold_percent` guards: skip when user
|
|
2652
2816
|
* input is already queued (it would race the next turn), when the run was
|
|
2653
2817
|
* aborted, or during the failure cooldown — and never let a compaction
|
|
2654
|
-
* error surface in the completed turn.
|
|
2818
|
+
* error surface in the completed turn. Besides the size trigger, a newly
|
|
2819
|
+
* completed approved-plan step may compact when the cache-cost rule in
|
|
2820
|
+
* compaction/plan-step-policy.ts says the shrink pays for itself.
|
|
2655
2821
|
*/
|
|
2656
2822
|
maybeCompactPostTurn(creds) {
|
|
2657
2823
|
if (!this.settingsManager.get("autoCompact"))
|
|
@@ -2664,15 +2830,18 @@ export class AgentSession {
|
|
|
2664
2830
|
return;
|
|
2665
2831
|
if (Date.now() < this.compactionRetryAfter)
|
|
2666
2832
|
return;
|
|
2667
|
-
// One compaction per turn boundary: a pre-run or overflow-recovery
|
|
2668
|
-
// compaction already shrank this run's history — re-probing right after
|
|
2669
|
-
// the final response would only re-derive that decision.
|
|
2670
|
-
if (this.compactionOccurred)
|
|
2671
|
-
return;
|
|
2672
2833
|
const contextWindow = getContextWindow(this.model, {
|
|
2673
2834
|
provider: this.provider,
|
|
2674
2835
|
accountId: creds.accountId,
|
|
2675
2836
|
});
|
|
2837
|
+
// One compaction per turn boundary: a pre-run, in-flight or overflow
|
|
2838
|
+
// compaction already shrank this run's history — re-probing right after
|
|
2839
|
+
// the final response would only re-derive that decision. Still record the
|
|
2840
|
+
// final response's `[DONE:n]` steps so plan bookkeeping stays current.
|
|
2841
|
+
if (this.compactionOccurred) {
|
|
2842
|
+
this.observePlanStepProgress(this.messages, contextWindow, undefined);
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2676
2845
|
const policy = resolveCompactionPolicy({
|
|
2677
2846
|
provider: this.provider,
|
|
2678
2847
|
model: this.model,
|
|
@@ -2691,9 +2860,12 @@ export class AgentSession {
|
|
|
2691
2860
|
});
|
|
2692
2861
|
}
|
|
2693
2862
|
}
|
|
2694
|
-
|
|
2863
|
+
const planStep = this.observePlanStepProgress(this.messages, contextWindow, activeTokens);
|
|
2864
|
+
if (!shouldCompact(this.messages, contextWindow, policy.threshold, activeTokens, policy.targetTokens) &&
|
|
2865
|
+
!planStep?.compact)
|
|
2695
2866
|
return;
|
|
2696
2867
|
log("INFO", "compaction", "Post-turn compaction decision — compacting in background", {
|
|
2868
|
+
trigger: planStep?.compact ? `plan-step (${planStep.reason})` : "size",
|
|
2697
2869
|
provider: this.provider,
|
|
2698
2870
|
model: this.model,
|
|
2699
2871
|
transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
|
|
@@ -2747,6 +2919,7 @@ export class AgentSession {
|
|
|
2747
2919
|
approvedPlanPath: this.approvedPlanPath,
|
|
2748
2920
|
});
|
|
2749
2921
|
const originalCount = this.messages.length;
|
|
2922
|
+
const contextTokensBefore = estimateConversationTokens(this.messages);
|
|
2750
2923
|
this.eventBus.emit("compaction_start", { messageCount: originalCount });
|
|
2751
2924
|
let contextSelection;
|
|
2752
2925
|
const runCompactor = async () => {
|
|
@@ -2784,7 +2957,7 @@ export class AgentSession {
|
|
|
2784
2957
|
let sourceFingerprint = computeSourceFingerprint(this.messages);
|
|
2785
2958
|
const canonicalPath = await this.sessionManager.resolveCanonicalSession(conversationId, this.cwd);
|
|
2786
2959
|
if (canonicalPath && canonicalPath !== this.sessionPath) {
|
|
2787
|
-
const newest = await this.sessionManager.load(canonicalPath);
|
|
2960
|
+
const newest = await this.sessionManager.load(canonicalPath, { canonical: true });
|
|
2788
2961
|
if (newest.header.sourceFingerprint === sourceFingerprint) {
|
|
2789
2962
|
await this.adoptCompactionCheckpoint(newest);
|
|
2790
2963
|
this.lastCompactionCompacted = true;
|
|
@@ -2858,6 +3031,10 @@ export class AgentSession {
|
|
|
2858
3031
|
}
|
|
2859
3032
|
});
|
|
2860
3033
|
}
|
|
3034
|
+
if (this.lastCompactionCompacted) {
|
|
3035
|
+
this.cacheDiagnostics.noteEdit("compaction");
|
|
3036
|
+
this.recordPlanStepCompaction(contextTokensBefore);
|
|
3037
|
+
}
|
|
2861
3038
|
this.eventBus.emit("compaction_end", {
|
|
2862
3039
|
compacted: this.lastCompactionCompacted,
|
|
2863
3040
|
originalCount,
|
|
@@ -2877,6 +3054,7 @@ export class AgentSession {
|
|
|
2877
3054
|
});
|
|
2878
3055
|
}
|
|
2879
3056
|
async newSession(preserveConversation = false) {
|
|
3057
|
+
this.cacheDiagnostics.reset();
|
|
2880
3058
|
// Approved-plan execution is a clean checkpoint of the same conversation;
|
|
2881
3059
|
// explicit new sessions reset the conversation identity.
|
|
2882
3060
|
if (!preserveConversation) {
|
|
@@ -2924,6 +3102,7 @@ export class AgentSession {
|
|
|
2924
3102
|
}
|
|
2925
3103
|
async loadSession(sessionPath) {
|
|
2926
3104
|
await this.loadExistingSession(sessionPath);
|
|
3105
|
+
this.cacheDiagnostics.reset();
|
|
2927
3106
|
if (this.sessionId)
|
|
2928
3107
|
await this.subAgentManager?.hydrate(this.sessionId);
|
|
2929
3108
|
this.eventBus.emit("session_start", { sessionId: this.sessionId });
|
|
@@ -2954,6 +3133,7 @@ export class AgentSession {
|
|
|
2954
3133
|
const branchMessages = this.sessionManager.getMessages(loaded.entries, this.currentLeafId);
|
|
2955
3134
|
const systemMsg = this.messages[0];
|
|
2956
3135
|
this.messages = [systemMsg, ...branchMessages];
|
|
3136
|
+
this.cacheDiagnostics.reset();
|
|
2957
3137
|
this.lastPersistedIndex = this.messages.length;
|
|
2958
3138
|
// Reads made in the dropped messages are no longer in the model's context.
|
|
2959
3139
|
this.clearReadTracker?.();
|
|
@@ -3243,6 +3423,18 @@ export class AgentSession {
|
|
|
3243
3423
|
* the standard prompt therefore needs a rebuild. Custom and sub-agent
|
|
3244
3424
|
* prompts never render packs, so detection is skipped for them.
|
|
3245
3425
|
*/
|
|
3426
|
+
/**
|
|
3427
|
+
* Bring the system prompt to the exact state the next provider request will
|
|
3428
|
+
* send. Shared by real runs and {@link prewarm} so both see one prefix.
|
|
3429
|
+
*/
|
|
3430
|
+
async prepareSystemPromptForRequest() {
|
|
3431
|
+
// Languages are re-detected at each task boundary so a project scaffolded
|
|
3432
|
+
// during the previous turn gets its packs; the prompt is rebuilt only when
|
|
3433
|
+
// the set grows, keeping the cached prefix stable otherwise.
|
|
3434
|
+
if (this.refreshActiveLanguages())
|
|
3435
|
+
await this.rebuildSystemPromptInPlace();
|
|
3436
|
+
this.refreshSystemPromptTail();
|
|
3437
|
+
}
|
|
3246
3438
|
refreshActiveLanguages() {
|
|
3247
3439
|
if (this.customSystemPrompt || this.agentPrompt !== undefined)
|
|
3248
3440
|
return false;
|
|
@@ -3341,6 +3533,17 @@ export class AgentSession {
|
|
|
3341
3533
|
}));
|
|
3342
3534
|
}
|
|
3343
3535
|
async persistTurnMetric(event) {
|
|
3536
|
+
if (event.stopReason === "error")
|
|
3537
|
+
this.cacheDiagnostics.discardAttempt();
|
|
3538
|
+
const cache = this.cacheDiagnostics.complete(event.usage, event.timing);
|
|
3539
|
+
if (cache) {
|
|
3540
|
+
log("INFO", "cache", "Context cache outcome", {
|
|
3541
|
+
sessionId: this.sessionId || this.transportSessionId,
|
|
3542
|
+
provider: this.provider,
|
|
3543
|
+
model: this.model,
|
|
3544
|
+
data: JSON.stringify(cache),
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3344
3547
|
const payload = {
|
|
3345
3548
|
version: 1,
|
|
3346
3549
|
turn: event.turn,
|
|
@@ -3726,6 +3929,123 @@ export class AgentSession {
|
|
|
3726
3929
|
if (signal?.aborted)
|
|
3727
3930
|
this.managerAbortHandler();
|
|
3728
3931
|
}
|
|
3932
|
+
runLoopDepth = 0;
|
|
3933
|
+
lastRealRequestAt = 0;
|
|
3934
|
+
lastPrewarmAt = 0;
|
|
3935
|
+
prewarmController = null;
|
|
3936
|
+
/**
|
|
3937
|
+
* Best-effort Anthropic prompt-cache prewarm before the user's next turn
|
|
3938
|
+
* (desktop app calls this on the first keystroke after opening a chat or an
|
|
3939
|
+
* idle pause). Sends the exact request prefix the next real turn will use —
|
|
3940
|
+
* same system/tools/thinking/cache options — with `max_tokens: 1`, so the
|
|
3941
|
+
* first real reply is a cache read instead of a cold write.
|
|
3942
|
+
*/
|
|
3943
|
+
async prewarm(signal) {
|
|
3944
|
+
if (this.provider !== "anthropic")
|
|
3945
|
+
return { ok: false, reason: "provider" };
|
|
3946
|
+
if (this.settingsManager?.get("cachePrewarm") === false) {
|
|
3947
|
+
return { ok: false, reason: "disabled" };
|
|
3948
|
+
}
|
|
3949
|
+
if (this.runLoopDepth > 0)
|
|
3950
|
+
return { ok: false, reason: "run_active" };
|
|
3951
|
+
if (this.prewarmController)
|
|
3952
|
+
return { ok: false, reason: "in_flight" };
|
|
3953
|
+
const cacheRetention = this.isSpeedOptimized() ? "long" : "short";
|
|
3954
|
+
const ttlMs = (cacheRetention === "long" ? 60 : 5) * 60_000;
|
|
3955
|
+
const now = Date.now();
|
|
3956
|
+
if (now - Math.max(this.lastPrewarmAt, this.lastRealRequestAt) < ttlMs) {
|
|
3957
|
+
return { ok: false, reason: "cache_fresh" };
|
|
3958
|
+
}
|
|
3959
|
+
// Same preparation as the real run, BEFORE copying history: otherwise the
|
|
3960
|
+
// warmed system block differs from the next request (language packs are
|
|
3961
|
+
// detected at run start) and everything after it misses the cache.
|
|
3962
|
+
await this.prepareSystemPromptForRequest();
|
|
3963
|
+
// The await above yields: re-check that no run or other prewarm began.
|
|
3964
|
+
if (this.runLoopDepth > 0)
|
|
3965
|
+
return { ok: false, reason: "run_active" };
|
|
3966
|
+
if (this.prewarmController)
|
|
3967
|
+
return { ok: false, reason: "in_flight" };
|
|
3968
|
+
// End at the last user/tool message: the next real turn appends a new user
|
|
3969
|
+
// message after the trailing assistant reply, and its cache lookback hits
|
|
3970
|
+
// the entry written at this boundary. A request must end on a user turn.
|
|
3971
|
+
let end = this.messages.length;
|
|
3972
|
+
while (end > 0 && this.messages[end - 1]?.role === "assistant")
|
|
3973
|
+
end--;
|
|
3974
|
+
// The loop repairs tool pairing in place before every request; apply the
|
|
3975
|
+
// same repair to a copy so the warmed prefix is byte-identical to it.
|
|
3976
|
+
const messages = structuredClone(this.messages.slice(0, end));
|
|
3977
|
+
repairToolPairingAdjacent(messages);
|
|
3978
|
+
if (!messages.some((m) => m.role === "user" || m.role === "tool")) {
|
|
3979
|
+
return { ok: false, reason: "no_history" };
|
|
3980
|
+
}
|
|
3981
|
+
const tokens = estimateConversationTokens(messages);
|
|
3982
|
+
if (tokens < 4_000)
|
|
3983
|
+
return { ok: false, reason: "too_small" };
|
|
3984
|
+
const controller = new AbortController();
|
|
3985
|
+
const onAbort = () => controller.abort();
|
|
3986
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3987
|
+
this.prewarmController = controller;
|
|
3988
|
+
const started = Date.now();
|
|
3989
|
+
try {
|
|
3990
|
+
const creds = await this.authStorage.resolveCredentials(this.provider, {
|
|
3991
|
+
storageKeys: this.currentAuthStorageKeys(),
|
|
3992
|
+
});
|
|
3993
|
+
if (controller.signal.aborted || this.runLoopDepth > 0) {
|
|
3994
|
+
return { ok: false, reason: "aborted" };
|
|
3995
|
+
}
|
|
3996
|
+
const modelInfo = getModel(this.model);
|
|
3997
|
+
const result = stream({
|
|
3998
|
+
provider: this.provider,
|
|
3999
|
+
model: this.model,
|
|
4000
|
+
messages,
|
|
4001
|
+
tools: this.tools,
|
|
4002
|
+
webSearch: true,
|
|
4003
|
+
maxTokens: this.maxTokens,
|
|
4004
|
+
thinking: this.planModeRef.current
|
|
4005
|
+
? clampThinkingForPlanMode(this.thinkingLevel)
|
|
4006
|
+
: this.thinkingLevel,
|
|
4007
|
+
apiKey: creds.accessToken,
|
|
4008
|
+
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
4009
|
+
accountId: creds.accountId,
|
|
4010
|
+
transportSessionId: this.sessionId || this.transportSessionId,
|
|
4011
|
+
cacheRetention,
|
|
4012
|
+
promptCacheKey: this.getPromptCacheKey(),
|
|
4013
|
+
supportsImages: modelInfo?.supportsImages,
|
|
4014
|
+
supportsVideo: modelInfo?.supportsVideo,
|
|
4015
|
+
userAgent: await getClaudeCliUserAgent(),
|
|
4016
|
+
prewarm: true,
|
|
4017
|
+
signal: controller.signal,
|
|
4018
|
+
});
|
|
4019
|
+
const response = await result.response;
|
|
4020
|
+
const usage = response.usage;
|
|
4021
|
+
if (usage.inputTokens === 0 && usage.outputTokens === 0) {
|
|
4022
|
+
// gg-ai sent nothing: budget thinking can't stay identical at max_tokens 1.
|
|
4023
|
+
log("INFO", "prewarm", "Cache prewarm skipped: budget thinking", {
|
|
4024
|
+
model: this.model,
|
|
4025
|
+
});
|
|
4026
|
+
return { ok: false, reason: "thinking_budget_incompatible" };
|
|
4027
|
+
}
|
|
4028
|
+
this.lastPrewarmAt = Date.now();
|
|
4029
|
+
log("INFO", "prewarm", "Cache prewarm complete", {
|
|
4030
|
+
tokens: String(tokens),
|
|
4031
|
+
cacheRead: String(usage.cacheRead ?? 0),
|
|
4032
|
+
cacheWrite: String(usage.cacheWrite ?? 0),
|
|
4033
|
+
ms: String(Date.now() - started),
|
|
4034
|
+
});
|
|
4035
|
+
return { ok: true, reason: "warmed", usage };
|
|
4036
|
+
}
|
|
4037
|
+
catch (error) {
|
|
4038
|
+
if (isAbortError(error) || controller.signal.aborted)
|
|
4039
|
+
return { ok: false, reason: "aborted" };
|
|
4040
|
+
log("WARN", "prewarm", `Cache prewarm failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4041
|
+
return { ok: false, reason: "error" };
|
|
4042
|
+
}
|
|
4043
|
+
finally {
|
|
4044
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4045
|
+
if (this.prewarmController === controller)
|
|
4046
|
+
this.prewarmController = null;
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
3729
4049
|
/** True when speedProfile is "optimized" (1-h cache TTL + pre-warm), or the
|
|
3730
4050
|
* session was constructed with `forceLongCacheRetention` (Ken sessions). */
|
|
3731
4051
|
isSpeedOptimized() {
|
|
@@ -3760,6 +4080,7 @@ export class AgentSession {
|
|
|
3760
4080
|
// and leak a junk session file after teardown.
|
|
3761
4081
|
if (this.postTurnCompaction)
|
|
3762
4082
|
await this.postTurnCompaction;
|
|
4083
|
+
this.cacheDiagnostics.reset();
|
|
3763
4084
|
this.diagnosticsRecorder?.finalize();
|
|
3764
4085
|
this.managerAbortSignal?.removeEventListener("abort", this.managerAbortHandler);
|
|
3765
4086
|
this.processManager?.shutdownAll();
|
|
@@ -3799,8 +4120,10 @@ export class AgentSession {
|
|
|
3799
4120
|
// A stale physical checkpoint is only an address, not the conversation tip.
|
|
3800
4121
|
// Resolve every resume—not just over-threshold/deferred compaction resumes—
|
|
3801
4122
|
// before reading history so the next prompt cannot continue an old branch.
|
|
3802
|
-
const
|
|
3803
|
-
const loaded =
|
|
4123
|
+
const resolvedPath = await this.sessionManager.resolveCanonicalSession(sessionPath, this.cwd);
|
|
4124
|
+
const loaded = resolvedPath
|
|
4125
|
+
? await this.sessionManager.load(resolvedPath, { canonical: true })
|
|
4126
|
+
: await this.sessionManager.load(sessionPath);
|
|
3804
4127
|
// Use the leaf from the header to walk the correct branch
|
|
3805
4128
|
const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
|
|
3806
4129
|
const savedCompletionReview = [...loaded.entries]
|