@kenkaiiii/ggcoder 5.67.0 → 5.68.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/dist/app-sidecar.js +121 -3
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/agent-session.d.ts +49 -3
- package/dist/core/agent-session.js +305 -22
- 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/autopilot-verdict.d.ts +5 -1
- package/dist/core/autopilot-verdict.js +30 -13
- package/dist/core/autopilot-verdict.js.map +1 -1
- 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/ken-context.d.ts +7 -5
- package/dist/core/ken-context.js +106 -16
- package/dist/core/ken-context.js.map +1 -1
- package/dist/core/ken-prompt.js +24 -21
- package/dist/core/ken-prompt.js.map +1 -1
- 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.js +60 -29
- 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,5 +1,5 @@
|
|
|
1
1
|
import { type AgentTool } from "@kenkaiiii/gg-agent";
|
|
2
|
-
import { type Message, type MessageProvenance, type Provider, type ThinkingLevel } from "@kenkaiiii/gg-ai";
|
|
2
|
+
import { type Message, type MessageProvenance, type Provider, type Usage, type ThinkingLevel } from "@kenkaiiii/gg-ai";
|
|
3
3
|
import { EventBus } from "./event-bus.js";
|
|
4
4
|
import { type CompletionReview } from "./completion-review.js";
|
|
5
5
|
import { SlashCommandRegistry } from "./slash-commands.js";
|
|
@@ -299,6 +299,12 @@ export declare class AgentSession {
|
|
|
299
299
|
private postTurnCompaction?;
|
|
300
300
|
private lastCompactionCompacted;
|
|
301
301
|
private compactionRetryAfter;
|
|
302
|
+
/**
|
|
303
|
+
* SoL-Pi plan-step compaction bookkeeping (see compaction/plan-step-policy.ts).
|
|
304
|
+
* Step progress comes from `[DONE:n]` markers in assistant text — the same
|
|
305
|
+
* contract the approved-plan UI tracks.
|
|
306
|
+
*/
|
|
307
|
+
private planStepState;
|
|
302
308
|
/** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
|
|
303
309
|
private deferredCompactionPending;
|
|
304
310
|
/** Latest provider count, anchored to the assistant response it measured. */
|
|
@@ -601,8 +607,11 @@ export declare class AgentSession {
|
|
|
601
607
|
private getHookFollowUpMessages;
|
|
602
608
|
private reviewLspEvidence;
|
|
603
609
|
private withReviewLspEvidence;
|
|
604
|
-
/**
|
|
610
|
+
/** Wraps the real run: cancels an in-flight cache prewarm and tracks run
|
|
611
|
+
* activity / last real request time for {@link prewarm}. */
|
|
605
612
|
private runLoop;
|
|
613
|
+
/** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
|
|
614
|
+
private runLoopInner;
|
|
606
615
|
switchModel(provider: string, model: string): Promise<void>;
|
|
607
616
|
/**
|
|
608
617
|
* Record the switch as its own trailing message at the point it happened,
|
|
@@ -629,6 +638,20 @@ export declare class AgentSession {
|
|
|
629
638
|
* protect it.
|
|
630
639
|
*/
|
|
631
640
|
private settlePostTurnCompaction;
|
|
641
|
+
/**
|
|
642
|
+
* Advance plan-step bookkeeping over the messages added since the last
|
|
643
|
+
* observation and, when a plan step was newly completed (`[DONE:n]`), run
|
|
644
|
+
* the SoL-Pi cost rule. Called between model steps (in-flight) and once
|
|
645
|
+
* after the run; `scanIndex` and `doneSteps` make each message and each
|
|
646
|
+
* step count once across both paths. Returns undefined when no step
|
|
647
|
+
* completed since the last observation.
|
|
648
|
+
*/
|
|
649
|
+
private observePlanStepProgress;
|
|
650
|
+
/**
|
|
651
|
+
* Plan-step bookkeeping after ANY successful compaction (as in the bench:
|
|
652
|
+
* every compaction leaves a cache-write cost to be repaid by later savings).
|
|
653
|
+
*/
|
|
654
|
+
private recordPlanStepCompaction;
|
|
632
655
|
/**
|
|
633
656
|
* Post-turn compaction: once the final response has been delivered, compact
|
|
634
657
|
* in the background while the user reads the answer, instead of making the
|
|
@@ -637,7 +660,9 @@ export declare class AgentSession {
|
|
|
637
660
|
* Codex `model_post_turn_compact_threshold_percent` guards: skip when user
|
|
638
661
|
* input is already queued (it would race the next turn), when the run was
|
|
639
662
|
* aborted, or during the failure cooldown — and never let a compaction
|
|
640
|
-
* error surface in the completed turn.
|
|
663
|
+
* error surface in the completed turn. Besides the size trigger, a newly
|
|
664
|
+
* completed approved-plan step may compact when the cache-cost rule in
|
|
665
|
+
* compaction/plan-step-policy.ts says the shrink pays for itself.
|
|
641
666
|
*/
|
|
642
667
|
private maybeCompactPostTurn;
|
|
643
668
|
compact(existingCredentials?: {
|
|
@@ -789,6 +814,11 @@ export declare class AgentSession {
|
|
|
789
814
|
* the standard prompt therefore needs a rebuild. Custom and sub-agent
|
|
790
815
|
* prompts never render packs, so detection is skipped for them.
|
|
791
816
|
*/
|
|
817
|
+
/**
|
|
818
|
+
* Bring the system prompt to the exact state the next provider request will
|
|
819
|
+
* send. Shared by real runs and {@link prewarm} so both see one prefix.
|
|
820
|
+
*/
|
|
821
|
+
private prepareSystemPromptForRequest;
|
|
792
822
|
private refreshActiveLanguages;
|
|
793
823
|
/** Rebuild messages[0] from current plan-mode + approved-plan state. */
|
|
794
824
|
private rebuildSystemPromptInPlace;
|
|
@@ -924,6 +954,22 @@ export declare class AgentSession {
|
|
|
924
954
|
/** Replace the abort signal (e.g. after cancellation). */
|
|
925
955
|
setSignal(signal: AbortSignal): void;
|
|
926
956
|
private bindManagerCancellation;
|
|
957
|
+
private runLoopDepth;
|
|
958
|
+
private lastRealRequestAt;
|
|
959
|
+
private lastPrewarmAt;
|
|
960
|
+
private prewarmController;
|
|
961
|
+
/**
|
|
962
|
+
* Best-effort Anthropic prompt-cache prewarm before the user's next turn
|
|
963
|
+
* (desktop app calls this on the first keystroke after opening a chat or an
|
|
964
|
+
* idle pause). Sends the exact request prefix the next real turn will use —
|
|
965
|
+
* same system/tools/thinking/cache options — with `max_tokens: 1`, so the
|
|
966
|
+
* first real reply is a cache read instead of a cold write.
|
|
967
|
+
*/
|
|
968
|
+
prewarm(signal?: AbortSignal): Promise<{
|
|
969
|
+
ok: boolean;
|
|
970
|
+
reason: string;
|
|
971
|
+
usage?: Usage;
|
|
972
|
+
}>;
|
|
927
973
|
/** True when speedProfile is "optimized" (1-h cache TTL + pre-warm), or the
|
|
928
974
|
* session was constructed with `forceLongCacheRetention` (Ken sessions). */
|
|
929
975
|
private isSpeedOptimized;
|
|
@@ -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,12 @@ 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 { setEstimatorModel, calibrateEstimatorFromUsage } from "./compaction/token-estimator.js";
|
|
39
|
+
import { setEstimatorModel, calibrateEstimatorFromUsage, estimateConversationTokens, } from "./compaction/token-estimator.js";
|
|
40
40
|
import { calculateActiveContextTokens } from "./compaction/active-context.js";
|
|
41
41
|
import { resolveCompactionPolicy } from "./compaction/policy.js";
|
|
42
|
+
import { decidePlanStepCompaction, DEFAULT_CACHE_WRITE_READ_RATIO, PLAN_STEP_KEEP_TOKENS, } from "./compaction/plan-step-policy.js";
|
|
43
|
+
import { extractPlanSteps, findCompletedMarkers } from "../utils/plan-steps.js";
|
|
44
|
+
import { readFileSync } from "node:fs";
|
|
42
45
|
import { clampThinkingForPlanMode } from "./thinking-level.js";
|
|
43
46
|
import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
44
47
|
import { discoverAgents } from "./agents.js";
|
|
@@ -57,6 +60,7 @@ import { classifyVerificationCommand } from "./verification-evidence.js";
|
|
|
57
60
|
import { captureVerificationSnapshot } from "./verification-snapshot.js";
|
|
58
61
|
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
59
62
|
import { normalizeMessageImages } from "./message-images.js";
|
|
63
|
+
import { loadStreamRules } from "./stream-rules.js";
|
|
60
64
|
import crypto from "node:crypto";
|
|
61
65
|
import fs from "node:fs/promises";
|
|
62
66
|
import os from "node:os";
|
|
@@ -216,6 +220,25 @@ export class AgentSession {
|
|
|
216
220
|
postTurnCompaction;
|
|
217
221
|
lastCompactionCompacted = false;
|
|
218
222
|
compactionRetryAfter = 0;
|
|
223
|
+
/**
|
|
224
|
+
* SoL-Pi plan-step compaction bookkeeping (see compaction/plan-step-policy.ts).
|
|
225
|
+
* Step progress comes from `[DONE:n]` markers in assistant text — the same
|
|
226
|
+
* contract the approved-plan UI tracks.
|
|
227
|
+
*/
|
|
228
|
+
planStepState = {
|
|
229
|
+
planPath: undefined,
|
|
230
|
+
scanIndex: 0,
|
|
231
|
+
doneSteps: new Set(),
|
|
232
|
+
requestsInCompletedSteps: 0,
|
|
233
|
+
requestsThisStep: 0,
|
|
234
|
+
requests: 0,
|
|
235
|
+
grownTokens: 0,
|
|
236
|
+
lastContextTokens: 0,
|
|
237
|
+
compactions: 0,
|
|
238
|
+
writeCostBalance: 0,
|
|
239
|
+
savingPerRequest: 0,
|
|
240
|
+
requestsSinceLastCompaction: 0,
|
|
241
|
+
};
|
|
219
242
|
/** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
|
|
220
243
|
deferredCompactionPending = false;
|
|
221
244
|
/** Latest provider count, anchored to the assistant response it measured. */
|
|
@@ -923,6 +946,7 @@ export class AgentSession {
|
|
|
923
946
|
kind: "prompt",
|
|
924
947
|
visibility: "transcript",
|
|
925
948
|
}, options = {}) {
|
|
949
|
+
this.prewarmController?.abort();
|
|
926
950
|
await this.settlePostTurnCompaction();
|
|
927
951
|
await this.adoptDeferredCheckpointBeforePrompt();
|
|
928
952
|
const slash = await this.resolveSlashInput(content);
|
|
@@ -958,6 +982,7 @@ export class AgentSession {
|
|
|
958
982
|
* attachments are always a direct conversational turn.
|
|
959
983
|
*/
|
|
960
984
|
async promptWithAttachments(text, attachments) {
|
|
985
|
+
this.prewarmController?.abort();
|
|
961
986
|
await this.settlePostTurnCompaction();
|
|
962
987
|
await this.adoptDeferredCheckpointBeforePrompt();
|
|
963
988
|
const parts = this.buildAttachmentParts(text, attachments);
|
|
@@ -1157,7 +1182,14 @@ export class AgentSession {
|
|
|
1157
1182
|
if (call.sourceSnapshot === null)
|
|
1158
1183
|
this.verificationGate.requireFreshVerification(true, event.args.command);
|
|
1159
1184
|
}
|
|
1160
|
-
else
|
|
1185
|
+
else if ((classification.accepted && event.args.persist !== true) ||
|
|
1186
|
+
(!classification.accepted && classification.mayMutate)) {
|
|
1187
|
+
// Flag the workspace unknown only when tool_call_end can resolve
|
|
1188
|
+
// it: a bounded check records pass/fail, a file-rewriting command
|
|
1189
|
+
// bumps the revision. An unrecognized read-only check (`biome ci`)
|
|
1190
|
+
// or a persistent-shell run records nothing at the end, so
|
|
1191
|
+
// flagging it left verified work Unverified forever — and
|
|
1192
|
+
// autopilot silently refused every later turn.
|
|
1161
1193
|
this.verificationGate.requireFreshVerification(!classification.accepted && classification.mayMutate, event.args.command);
|
|
1162
1194
|
}
|
|
1163
1195
|
await this.persistVerificationState();
|
|
@@ -2028,14 +2060,22 @@ export class AgentSession {
|
|
|
2028
2060
|
content: `${String(message.content)}\n\n${notes.join(" ")}`,
|
|
2029
2061
|
};
|
|
2030
2062
|
}
|
|
2031
|
-
/**
|
|
2063
|
+
/** Wraps the real run: cancels an in-flight cache prewarm and tracks run
|
|
2064
|
+
* activity / last real request time for {@link prewarm}. */
|
|
2032
2065
|
async runLoop(options = {}) {
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2066
|
+
this.prewarmController?.abort();
|
|
2067
|
+
this.runLoopDepth++;
|
|
2068
|
+
try {
|
|
2069
|
+
await this.runLoopInner(options);
|
|
2070
|
+
}
|
|
2071
|
+
finally {
|
|
2072
|
+
this.runLoopDepth--;
|
|
2073
|
+
this.lastRealRequestAt = Date.now();
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
/** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
|
|
2077
|
+
async runLoopInner(options = {}) {
|
|
2078
|
+
await this.prepareSystemPromptForRequest();
|
|
2039
2079
|
// One-shot cache-key marker per session so turn_end cacheRead numbers
|
|
2040
2080
|
// in the log can be traced back to a specific routing namespace —
|
|
2041
2081
|
// particularly useful when sub-agents inherit `parentKey:subagent`.
|
|
@@ -2130,11 +2170,14 @@ export class AgentSession {
|
|
|
2130
2170
|
}
|
|
2131
2171
|
const userAgent = this.provider === "anthropic" ? await getClaudeCliUserAgent() : undefined;
|
|
2132
2172
|
const loopMessages = await this.prepareDynamicContext();
|
|
2173
|
+
// Re-read every run so rule-file edits apply on the next turn; a few small files.
|
|
2174
|
+
const streamRules = await loadStreamRules(this.cwd);
|
|
2133
2175
|
const runAgentLoop = async (apiKey, accountId, projectId) => {
|
|
2134
2176
|
lastResolvedAccessToken = apiKey;
|
|
2135
2177
|
const modelInfo = getModel(this.model);
|
|
2136
2178
|
const effectiveBaseUrl = this.baseUrl ?? creds.baseUrl;
|
|
2137
2179
|
const generator = agentLoop(loopMessages, {
|
|
2180
|
+
...(streamRules.length > 0 ? { streamRules: { rules: streamRules } } : {}),
|
|
2138
2181
|
provider: this.provider,
|
|
2139
2182
|
model: this.model,
|
|
2140
2183
|
tools: options.disableTools ? [] : this.tools,
|
|
@@ -2258,6 +2301,9 @@ export class AgentSession {
|
|
|
2258
2301
|
usage,
|
|
2259
2302
|
pendingMessages,
|
|
2260
2303
|
});
|
|
2304
|
+
// An approved plan runs as ONE run, so step boundaries are only
|
|
2305
|
+
// visible here, between model steps — not after the run ends.
|
|
2306
|
+
const planStep = this.observePlanStepProgress(messages, contextWindow, activeTokens);
|
|
2261
2307
|
log("INFO", "compaction", "In-flight compaction decision", {
|
|
2262
2308
|
provider: this.provider,
|
|
2263
2309
|
model: this.model,
|
|
@@ -2265,8 +2311,10 @@ export class AgentSession {
|
|
|
2265
2311
|
contextWindow: String(contextWindow),
|
|
2266
2312
|
activeTokens: String(activeTokens),
|
|
2267
2313
|
triggerLimit: String(policy.targetTokens),
|
|
2314
|
+
...(planStep ? { planStep: `${planStep.compact} (${planStep.reason})` } : {}),
|
|
2268
2315
|
});
|
|
2269
|
-
if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens, policy.targetTokens)
|
|
2316
|
+
if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens, policy.targetTokens) &&
|
|
2317
|
+
!planStep?.compact)
|
|
2270
2318
|
return messages;
|
|
2271
2319
|
}
|
|
2272
2320
|
// compact() operates on this.messages, while an earlier transform may
|
|
@@ -2587,7 +2635,7 @@ export class AgentSession {
|
|
|
2587
2635
|
const canonicalPath = await this.sessionManager.resolveCanonicalSession(this.conversationId, this.cwd);
|
|
2588
2636
|
if (!canonicalPath || canonicalPath === this.sessionPath)
|
|
2589
2637
|
return;
|
|
2590
|
-
await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath));
|
|
2638
|
+
await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath, { canonical: true }));
|
|
2591
2639
|
}
|
|
2592
2640
|
async persistCompactionCheckpoint(sourceFingerprint, result) {
|
|
2593
2641
|
const parentSessionId = this.sessionId || undefined;
|
|
@@ -2636,6 +2684,99 @@ export class AgentSession {
|
|
|
2636
2684
|
if (this.postTurnCompaction)
|
|
2637
2685
|
await this.postTurnCompaction;
|
|
2638
2686
|
}
|
|
2687
|
+
/**
|
|
2688
|
+
* Advance plan-step bookkeeping over the messages added since the last
|
|
2689
|
+
* observation and, when a plan step was newly completed (`[DONE:n]`), run
|
|
2690
|
+
* the SoL-Pi cost rule. Called between model steps (in-flight) and once
|
|
2691
|
+
* after the run; `scanIndex` and `doneSteps` make each message and each
|
|
2692
|
+
* step count once across both paths. Returns undefined when no step
|
|
2693
|
+
* completed since the last observation.
|
|
2694
|
+
*/
|
|
2695
|
+
observePlanStepProgress(messages, contextWindow, activeTokens) {
|
|
2696
|
+
const st = this.planStepState;
|
|
2697
|
+
const planPath = this.approvedPlanPath;
|
|
2698
|
+
if (st.planPath !== planPath) {
|
|
2699
|
+
st.planPath = planPath;
|
|
2700
|
+
st.doneSteps = new Set();
|
|
2701
|
+
st.requestsInCompletedSteps = 0;
|
|
2702
|
+
st.requestsThisStep = 0;
|
|
2703
|
+
}
|
|
2704
|
+
if (st.scanIndex > messages.length)
|
|
2705
|
+
st.scanIndex = 0;
|
|
2706
|
+
let requests = 0;
|
|
2707
|
+
const newlyDone = [];
|
|
2708
|
+
for (let i = st.scanIndex; i < messages.length; i++) {
|
|
2709
|
+
const msg = messages[i];
|
|
2710
|
+
if (msg?.role !== "assistant")
|
|
2711
|
+
continue;
|
|
2712
|
+
requests++;
|
|
2713
|
+
const text = typeof msg.content === "string"
|
|
2714
|
+
? msg.content
|
|
2715
|
+
: msg.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
|
|
2716
|
+
for (const step of findCompletedMarkers(text)) {
|
|
2717
|
+
if (!st.doneSteps.has(step))
|
|
2718
|
+
newlyDone.push(step);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
st.scanIndex = messages.length;
|
|
2722
|
+
const contextTokens = activeTokens ?? estimateConversationTokens(messages);
|
|
2723
|
+
if (st.lastContextTokens > 0)
|
|
2724
|
+
st.grownTokens += Math.max(0, contextTokens - st.lastContextTokens);
|
|
2725
|
+
st.lastContextTokens = contextTokens;
|
|
2726
|
+
st.requests += requests;
|
|
2727
|
+
st.requestsThisStep += requests;
|
|
2728
|
+
st.requestsSinceLastCompaction += requests;
|
|
2729
|
+
if (st.writeCostBalance > 0)
|
|
2730
|
+
st.writeCostBalance -= st.savingPerRequest * requests;
|
|
2731
|
+
if (!planPath || newlyDone.length === 0)
|
|
2732
|
+
return undefined;
|
|
2733
|
+
for (const step of newlyDone)
|
|
2734
|
+
st.doneSteps.add(step);
|
|
2735
|
+
st.requestsInCompletedSteps += st.requestsThisStep;
|
|
2736
|
+
st.requestsThisStep = 0;
|
|
2737
|
+
let planText;
|
|
2738
|
+
try {
|
|
2739
|
+
planText = readFileSync(planPath, "utf8");
|
|
2740
|
+
}
|
|
2741
|
+
catch {
|
|
2742
|
+
return undefined;
|
|
2743
|
+
}
|
|
2744
|
+
const totalSteps = extractPlanSteps(planText).length;
|
|
2745
|
+
const decision = decidePlanStepCompaction({
|
|
2746
|
+
contextTokens,
|
|
2747
|
+
keptTailTokens: PLAN_STEP_KEEP_TOKENS,
|
|
2748
|
+
contextWindow,
|
|
2749
|
+
// The model registry carries no cache price fields; use the SoL-Pi default.
|
|
2750
|
+
cacheWriteReadRatio: DEFAULT_CACHE_WRITE_READ_RATIO,
|
|
2751
|
+
stepsCompleted: st.doneSteps.size,
|
|
2752
|
+
stepsRemaining: Math.max(0, totalSteps - st.doneSteps.size),
|
|
2753
|
+
requestsInCompletedSteps: st.requestsInCompletedSteps,
|
|
2754
|
+
requests: st.requests,
|
|
2755
|
+
grownTokens: st.grownTokens,
|
|
2756
|
+
priorCompactions: st.compactions,
|
|
2757
|
+
writeCostBalance: st.writeCostBalance,
|
|
2758
|
+
requestsSinceLastCompaction: st.compactions > 0 ? st.requestsSinceLastCompaction : undefined,
|
|
2759
|
+
});
|
|
2760
|
+
log("INFO", "compaction", "Plan-step compaction decision", {
|
|
2761
|
+
compact: String(decision.compact),
|
|
2762
|
+
reason: decision.reason,
|
|
2763
|
+
});
|
|
2764
|
+
return decision;
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Plan-step bookkeeping after ANY successful compaction (as in the bench:
|
|
2768
|
+
* every compaction leaves a cache-write cost to be repaid by later savings).
|
|
2769
|
+
*/
|
|
2770
|
+
recordPlanStepCompaction(contextBefore) {
|
|
2771
|
+
const st = this.planStepState;
|
|
2772
|
+
const after = estimateConversationTokens(this.messages);
|
|
2773
|
+
st.scanIndex = this.messages.length;
|
|
2774
|
+
st.lastContextTokens = after;
|
|
2775
|
+
st.requestsSinceLastCompaction = 0;
|
|
2776
|
+
st.compactions++;
|
|
2777
|
+
st.writeCostBalance += after * (DEFAULT_CACHE_WRITE_READ_RATIO - 1);
|
|
2778
|
+
st.savingPerRequest = Math.max(0, contextBefore - after);
|
|
2779
|
+
}
|
|
2639
2780
|
/**
|
|
2640
2781
|
* Post-turn compaction: once the final response has been delivered, compact
|
|
2641
2782
|
* in the background while the user reads the answer, instead of making the
|
|
@@ -2644,7 +2785,9 @@ export class AgentSession {
|
|
|
2644
2785
|
* Codex `model_post_turn_compact_threshold_percent` guards: skip when user
|
|
2645
2786
|
* input is already queued (it would race the next turn), when the run was
|
|
2646
2787
|
* aborted, or during the failure cooldown — and never let a compaction
|
|
2647
|
-
* error surface in the completed turn.
|
|
2788
|
+
* error surface in the completed turn. Besides the size trigger, a newly
|
|
2789
|
+
* completed approved-plan step may compact when the cache-cost rule in
|
|
2790
|
+
* compaction/plan-step-policy.ts says the shrink pays for itself.
|
|
2648
2791
|
*/
|
|
2649
2792
|
maybeCompactPostTurn(creds) {
|
|
2650
2793
|
if (!this.settingsManager.get("autoCompact"))
|
|
@@ -2657,15 +2800,18 @@ export class AgentSession {
|
|
|
2657
2800
|
return;
|
|
2658
2801
|
if (Date.now() < this.compactionRetryAfter)
|
|
2659
2802
|
return;
|
|
2660
|
-
// One compaction per turn boundary: a pre-run or overflow-recovery
|
|
2661
|
-
// compaction already shrank this run's history — re-probing right after
|
|
2662
|
-
// the final response would only re-derive that decision.
|
|
2663
|
-
if (this.compactionOccurred)
|
|
2664
|
-
return;
|
|
2665
2803
|
const contextWindow = getContextWindow(this.model, {
|
|
2666
2804
|
provider: this.provider,
|
|
2667
2805
|
accountId: creds.accountId,
|
|
2668
2806
|
});
|
|
2807
|
+
// One compaction per turn boundary: a pre-run, in-flight or overflow
|
|
2808
|
+
// compaction already shrank this run's history — re-probing right after
|
|
2809
|
+
// the final response would only re-derive that decision. Still record the
|
|
2810
|
+
// final response's `[DONE:n]` steps so plan bookkeeping stays current.
|
|
2811
|
+
if (this.compactionOccurred) {
|
|
2812
|
+
this.observePlanStepProgress(this.messages, contextWindow, undefined);
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2669
2815
|
const policy = resolveCompactionPolicy({
|
|
2670
2816
|
provider: this.provider,
|
|
2671
2817
|
model: this.model,
|
|
@@ -2684,9 +2830,12 @@ export class AgentSession {
|
|
|
2684
2830
|
});
|
|
2685
2831
|
}
|
|
2686
2832
|
}
|
|
2687
|
-
|
|
2833
|
+
const planStep = this.observePlanStepProgress(this.messages, contextWindow, activeTokens);
|
|
2834
|
+
if (!shouldCompact(this.messages, contextWindow, policy.threshold, activeTokens, policy.targetTokens) &&
|
|
2835
|
+
!planStep?.compact)
|
|
2688
2836
|
return;
|
|
2689
2837
|
log("INFO", "compaction", "Post-turn compaction decision — compacting in background", {
|
|
2838
|
+
trigger: planStep?.compact ? `plan-step (${planStep.reason})` : "size",
|
|
2690
2839
|
provider: this.provider,
|
|
2691
2840
|
model: this.model,
|
|
2692
2841
|
transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
|
|
@@ -2740,6 +2889,7 @@ export class AgentSession {
|
|
|
2740
2889
|
approvedPlanPath: this.approvedPlanPath,
|
|
2741
2890
|
});
|
|
2742
2891
|
const originalCount = this.messages.length;
|
|
2892
|
+
const contextTokensBefore = estimateConversationTokens(this.messages);
|
|
2743
2893
|
this.eventBus.emit("compaction_start", { messageCount: originalCount });
|
|
2744
2894
|
let contextSelection;
|
|
2745
2895
|
const runCompactor = async () => {
|
|
@@ -2777,7 +2927,7 @@ export class AgentSession {
|
|
|
2777
2927
|
let sourceFingerprint = computeSourceFingerprint(this.messages);
|
|
2778
2928
|
const canonicalPath = await this.sessionManager.resolveCanonicalSession(conversationId, this.cwd);
|
|
2779
2929
|
if (canonicalPath && canonicalPath !== this.sessionPath) {
|
|
2780
|
-
const newest = await this.sessionManager.load(canonicalPath);
|
|
2930
|
+
const newest = await this.sessionManager.load(canonicalPath, { canonical: true });
|
|
2781
2931
|
if (newest.header.sourceFingerprint === sourceFingerprint) {
|
|
2782
2932
|
await this.adoptCompactionCheckpoint(newest);
|
|
2783
2933
|
this.lastCompactionCompacted = true;
|
|
@@ -2851,6 +3001,8 @@ export class AgentSession {
|
|
|
2851
3001
|
}
|
|
2852
3002
|
});
|
|
2853
3003
|
}
|
|
3004
|
+
if (this.lastCompactionCompacted)
|
|
3005
|
+
this.recordPlanStepCompaction(contextTokensBefore);
|
|
2854
3006
|
this.eventBus.emit("compaction_end", {
|
|
2855
3007
|
compacted: this.lastCompactionCompacted,
|
|
2856
3008
|
originalCount,
|
|
@@ -3236,6 +3388,18 @@ export class AgentSession {
|
|
|
3236
3388
|
* the standard prompt therefore needs a rebuild. Custom and sub-agent
|
|
3237
3389
|
* prompts never render packs, so detection is skipped for them.
|
|
3238
3390
|
*/
|
|
3391
|
+
/**
|
|
3392
|
+
* Bring the system prompt to the exact state the next provider request will
|
|
3393
|
+
* send. Shared by real runs and {@link prewarm} so both see one prefix.
|
|
3394
|
+
*/
|
|
3395
|
+
async prepareSystemPromptForRequest() {
|
|
3396
|
+
// Languages are re-detected at each task boundary so a project scaffolded
|
|
3397
|
+
// during the previous turn gets its packs; the prompt is rebuilt only when
|
|
3398
|
+
// the set grows, keeping the cached prefix stable otherwise.
|
|
3399
|
+
if (this.refreshActiveLanguages())
|
|
3400
|
+
await this.rebuildSystemPromptInPlace();
|
|
3401
|
+
this.refreshSystemPromptTail();
|
|
3402
|
+
}
|
|
3239
3403
|
refreshActiveLanguages() {
|
|
3240
3404
|
if (this.customSystemPrompt || this.agentPrompt !== undefined)
|
|
3241
3405
|
return false;
|
|
@@ -3719,6 +3883,123 @@ export class AgentSession {
|
|
|
3719
3883
|
if (signal?.aborted)
|
|
3720
3884
|
this.managerAbortHandler();
|
|
3721
3885
|
}
|
|
3886
|
+
runLoopDepth = 0;
|
|
3887
|
+
lastRealRequestAt = 0;
|
|
3888
|
+
lastPrewarmAt = 0;
|
|
3889
|
+
prewarmController = null;
|
|
3890
|
+
/**
|
|
3891
|
+
* Best-effort Anthropic prompt-cache prewarm before the user's next turn
|
|
3892
|
+
* (desktop app calls this on the first keystroke after opening a chat or an
|
|
3893
|
+
* idle pause). Sends the exact request prefix the next real turn will use —
|
|
3894
|
+
* same system/tools/thinking/cache options — with `max_tokens: 1`, so the
|
|
3895
|
+
* first real reply is a cache read instead of a cold write.
|
|
3896
|
+
*/
|
|
3897
|
+
async prewarm(signal) {
|
|
3898
|
+
if (this.provider !== "anthropic")
|
|
3899
|
+
return { ok: false, reason: "provider" };
|
|
3900
|
+
if (this.settingsManager?.get("cachePrewarm") === false) {
|
|
3901
|
+
return { ok: false, reason: "disabled" };
|
|
3902
|
+
}
|
|
3903
|
+
if (this.runLoopDepth > 0)
|
|
3904
|
+
return { ok: false, reason: "run_active" };
|
|
3905
|
+
if (this.prewarmController)
|
|
3906
|
+
return { ok: false, reason: "in_flight" };
|
|
3907
|
+
const cacheRetention = this.isSpeedOptimized() ? "long" : "short";
|
|
3908
|
+
const ttlMs = (cacheRetention === "long" ? 60 : 5) * 60_000;
|
|
3909
|
+
const now = Date.now();
|
|
3910
|
+
if (now - Math.max(this.lastPrewarmAt, this.lastRealRequestAt) < ttlMs) {
|
|
3911
|
+
return { ok: false, reason: "cache_fresh" };
|
|
3912
|
+
}
|
|
3913
|
+
// Same preparation as the real run, BEFORE copying history: otherwise the
|
|
3914
|
+
// warmed system block differs from the next request (language packs are
|
|
3915
|
+
// detected at run start) and everything after it misses the cache.
|
|
3916
|
+
await this.prepareSystemPromptForRequest();
|
|
3917
|
+
// The await above yields: re-check that no run or other prewarm began.
|
|
3918
|
+
if (this.runLoopDepth > 0)
|
|
3919
|
+
return { ok: false, reason: "run_active" };
|
|
3920
|
+
if (this.prewarmController)
|
|
3921
|
+
return { ok: false, reason: "in_flight" };
|
|
3922
|
+
// End at the last user/tool message: the next real turn appends a new user
|
|
3923
|
+
// message after the trailing assistant reply, and its cache lookback hits
|
|
3924
|
+
// the entry written at this boundary. A request must end on a user turn.
|
|
3925
|
+
let end = this.messages.length;
|
|
3926
|
+
while (end > 0 && this.messages[end - 1]?.role === "assistant")
|
|
3927
|
+
end--;
|
|
3928
|
+
// The loop repairs tool pairing in place before every request; apply the
|
|
3929
|
+
// same repair to a copy so the warmed prefix is byte-identical to it.
|
|
3930
|
+
const messages = structuredClone(this.messages.slice(0, end));
|
|
3931
|
+
repairToolPairingAdjacent(messages);
|
|
3932
|
+
if (!messages.some((m) => m.role === "user" || m.role === "tool")) {
|
|
3933
|
+
return { ok: false, reason: "no_history" };
|
|
3934
|
+
}
|
|
3935
|
+
const tokens = estimateConversationTokens(messages);
|
|
3936
|
+
if (tokens < 4_000)
|
|
3937
|
+
return { ok: false, reason: "too_small" };
|
|
3938
|
+
const controller = new AbortController();
|
|
3939
|
+
const onAbort = () => controller.abort();
|
|
3940
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3941
|
+
this.prewarmController = controller;
|
|
3942
|
+
const started = Date.now();
|
|
3943
|
+
try {
|
|
3944
|
+
const creds = await this.authStorage.resolveCredentials(this.provider, {
|
|
3945
|
+
storageKeys: this.currentAuthStorageKeys(),
|
|
3946
|
+
});
|
|
3947
|
+
if (controller.signal.aborted || this.runLoopDepth > 0) {
|
|
3948
|
+
return { ok: false, reason: "aborted" };
|
|
3949
|
+
}
|
|
3950
|
+
const modelInfo = getModel(this.model);
|
|
3951
|
+
const result = stream({
|
|
3952
|
+
provider: this.provider,
|
|
3953
|
+
model: this.model,
|
|
3954
|
+
messages,
|
|
3955
|
+
tools: this.tools,
|
|
3956
|
+
webSearch: true,
|
|
3957
|
+
maxTokens: this.maxTokens,
|
|
3958
|
+
thinking: this.planModeRef.current
|
|
3959
|
+
? clampThinkingForPlanMode(this.thinkingLevel)
|
|
3960
|
+
: this.thinkingLevel,
|
|
3961
|
+
apiKey: creds.accessToken,
|
|
3962
|
+
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
3963
|
+
accountId: creds.accountId,
|
|
3964
|
+
transportSessionId: this.sessionId || this.transportSessionId,
|
|
3965
|
+
cacheRetention,
|
|
3966
|
+
promptCacheKey: this.getPromptCacheKey(),
|
|
3967
|
+
supportsImages: modelInfo?.supportsImages,
|
|
3968
|
+
supportsVideo: modelInfo?.supportsVideo,
|
|
3969
|
+
userAgent: await getClaudeCliUserAgent(),
|
|
3970
|
+
prewarm: true,
|
|
3971
|
+
signal: controller.signal,
|
|
3972
|
+
});
|
|
3973
|
+
const response = await result.response;
|
|
3974
|
+
const usage = response.usage;
|
|
3975
|
+
if (usage.inputTokens === 0 && usage.outputTokens === 0) {
|
|
3976
|
+
// gg-ai sent nothing: budget thinking can't stay identical at max_tokens 1.
|
|
3977
|
+
log("INFO", "prewarm", "Cache prewarm skipped: budget thinking", {
|
|
3978
|
+
model: this.model,
|
|
3979
|
+
});
|
|
3980
|
+
return { ok: false, reason: "thinking_budget_incompatible" };
|
|
3981
|
+
}
|
|
3982
|
+
this.lastPrewarmAt = Date.now();
|
|
3983
|
+
log("INFO", "prewarm", "Cache prewarm complete", {
|
|
3984
|
+
tokens: String(tokens),
|
|
3985
|
+
cacheRead: String(usage.cacheRead ?? 0),
|
|
3986
|
+
cacheWrite: String(usage.cacheWrite ?? 0),
|
|
3987
|
+
ms: String(Date.now() - started),
|
|
3988
|
+
});
|
|
3989
|
+
return { ok: true, reason: "warmed", usage };
|
|
3990
|
+
}
|
|
3991
|
+
catch (error) {
|
|
3992
|
+
if (isAbortError(error) || controller.signal.aborted)
|
|
3993
|
+
return { ok: false, reason: "aborted" };
|
|
3994
|
+
log("WARN", "prewarm", `Cache prewarm failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3995
|
+
return { ok: false, reason: "error" };
|
|
3996
|
+
}
|
|
3997
|
+
finally {
|
|
3998
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3999
|
+
if (this.prewarmController === controller)
|
|
4000
|
+
this.prewarmController = null;
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
3722
4003
|
/** True when speedProfile is "optimized" (1-h cache TTL + pre-warm), or the
|
|
3723
4004
|
* session was constructed with `forceLongCacheRetention` (Ken sessions). */
|
|
3724
4005
|
isSpeedOptimized() {
|
|
@@ -3792,8 +4073,10 @@ export class AgentSession {
|
|
|
3792
4073
|
// A stale physical checkpoint is only an address, not the conversation tip.
|
|
3793
4074
|
// Resolve every resume—not just over-threshold/deferred compaction resumes—
|
|
3794
4075
|
// before reading history so the next prompt cannot continue an old branch.
|
|
3795
|
-
const
|
|
3796
|
-
const loaded =
|
|
4076
|
+
const resolvedPath = await this.sessionManager.resolveCanonicalSession(sessionPath, this.cwd);
|
|
4077
|
+
const loaded = resolvedPath
|
|
4078
|
+
? await this.sessionManager.load(resolvedPath, { canonical: true })
|
|
4079
|
+
: await this.sessionManager.load(sessionPath);
|
|
3797
4080
|
// Use the leaf from the header to walk the correct branch
|
|
3798
4081
|
const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
|
|
3799
4082
|
const savedCompletionReview = [...loaded.entries]
|