@kenkaiiii/ggcoder 5.60.2 → 5.60.4

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.
@@ -255,6 +255,15 @@ export declare class AgentSession {
255
255
  * only on a real edge. */
256
256
  private verificationArmed;
257
257
  private compactionOccurred;
258
+ /**
259
+ * Re-grounding carry-over for post-turn compaction. `resetHookState` clears
260
+ * `compactionOccurred` at run start — before the injection point — so a
261
+ * background compaction that finished after the final response parks its
262
+ * "re-ground next turn" signal here for pickup at the next run start.
263
+ */
264
+ private compactionArmedForNextRun;
265
+ /** In-flight post-turn (background) compaction, if any. */
266
+ private postTurnCompaction?;
258
267
  private lastCompactionCompacted;
259
268
  private compactionRetryAfter;
260
269
  /** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
@@ -569,6 +578,26 @@ export declare class AgentSession {
569
578
  /** Canonicalize a deferred restore before a new prompt can fork stale history. */
570
579
  private adoptDeferredCheckpointBeforePrompt;
571
580
  private persistCompactionCheckpoint;
581
+ /**
582
+ * Wait out any in-flight post-turn background compaction before a new
583
+ * entry point mutates `this.messages` (prompt, attachments, model switch).
584
+ * The background compact() snapshots and then REPLACES the array, so a
585
+ * message pushed while it runs would be silently dropped from the live
586
+ * history — the pre-run await in runLoop() sits after the push and cannot
587
+ * protect it.
588
+ */
589
+ private settlePostTurnCompaction;
590
+ /**
591
+ * Post-turn compaction: once the final response has been delivered, compact
592
+ * in the background while the user reads the answer, instead of making the
593
+ * next prompt pay the summarizer latency up front (the pre-run path stays as
594
+ * the backstop, so a skipped or failed attempt costs nothing). Mirrors the
595
+ * Codex `model_post_turn_compact_threshold_percent` guards: skip when user
596
+ * input is already queued (it would race the next turn), when the run was
597
+ * aborted, or during the failure cooldown — and never let a compaction
598
+ * error surface in the completed turn.
599
+ */
600
+ private maybeCompactPostTurn;
572
601
  compact(existingCredentials?: {
573
602
  accessToken: string;
574
603
  accountId?: string;
@@ -37,6 +37,7 @@ import { log } from "./logger.js";
37
37
  import { setEstimatorModel, calibrateEstimatorFromUsage } from "./compaction/token-estimator.js";
38
38
  import { calculateActiveContextTokens } from "./compaction/active-context.js";
39
39
  import { resolveCompactionPolicy } from "./compaction/policy.js";
40
+ import { clampThinkingForPlanMode } from "./thinking-level.js";
40
41
  import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
41
42
  import { discoverAgents } from "./agents.js";
42
43
  import { enhancePrompt } from "../utils/prompt-enhancer.js";
@@ -199,6 +200,15 @@ export class AgentSession {
199
200
  * only on a real edge. */
200
201
  verificationArmed = false;
201
202
  compactionOccurred = false;
203
+ /**
204
+ * Re-grounding carry-over for post-turn compaction. `resetHookState` clears
205
+ * `compactionOccurred` at run start — before the injection point — so a
206
+ * background compaction that finished after the final response parks its
207
+ * "re-ground next turn" signal here for pickup at the next run start.
208
+ */
209
+ compactionArmedForNextRun = false;
210
+ /** In-flight post-turn (background) compaction, if any. */
211
+ postTurnCompaction;
202
212
  lastCompactionCompacted = false;
203
213
  compactionRetryAfter = 0;
204
214
  /** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
@@ -895,6 +905,7 @@ export class AgentSession {
895
905
  kind: "prompt",
896
906
  visibility: "transcript",
897
907
  }, options = {}) {
908
+ await this.settlePostTurnCompaction();
898
909
  await this.adoptDeferredCheckpointBeforePrompt();
899
910
  const slash = await this.resolveSlashInput(content);
900
911
  if (slash?.kind === "template") {
@@ -929,6 +940,7 @@ export class AgentSession {
929
940
  * attachments are always a direct conversational turn.
930
941
  */
931
942
  async promptWithAttachments(text, attachments) {
943
+ await this.settlePostTurnCompaction();
932
944
  await this.adoptDeferredCheckpointBeforePrompt();
933
945
  const parts = this.buildAttachmentParts(text, attachments);
934
946
  if (parts.length === 0)
@@ -1069,6 +1081,13 @@ export class AgentSession {
1069
1081
  this.backgroundVerification.delete(id);
1070
1082
  }
1071
1083
  this.compactionOccurred = false;
1084
+ // Post-turn compaction may have landed between runs — adopt its armed
1085
+ // signal so the re-grounding hook fires for this run exactly as it would
1086
+ // after a pre-run compaction.
1087
+ if (this.compactionArmedForNextRun) {
1088
+ this.compactionArmedForNextRun = false;
1089
+ this.compactionOccurred = true;
1090
+ }
1072
1091
  this.originalRequest = originalRequest;
1073
1092
  }
1074
1093
  /**
@@ -1936,6 +1955,11 @@ export class AgentSession {
1936
1955
  // of the public API model window. Failed/no-op attempts cool down across
1937
1956
  // prompts; provider overflow recovery still bypasses this path entirely.
1938
1957
  if (this.settingsManager.get("autoCompact") && Date.now() >= this.compactionRetryAfter) {
1958
+ // A post-turn compaction may still be running in the background. Let it
1959
+ // settle first: it usually already compacted this history, so the
1960
+ // decision below turns into a no-op instead of duplicate work.
1961
+ if (this.postTurnCompaction)
1962
+ await this.postTurnCompaction;
1939
1963
  const contextWindow = getContextWindow(this.model, {
1940
1964
  provider: this.provider,
1941
1965
  accountId: creds.accountId,
@@ -2003,7 +2027,13 @@ export class AgentSession {
2003
2027
  maxTokens: this.maxTokens,
2004
2028
  maxTurns: this.opts.maxTurns,
2005
2029
  maxTurnExtensions: this.opts.maxTurnExtensions,
2006
- thinking: this.thinkingLevel,
2030
+ // Plan mode caps effort at medium (Codex `plan_mode_reasoning_effort`
2031
+ // preset): read-only exploration doesn't need xhigh/max reasoning, and
2032
+ // deep-reasoning models left at the ceiling burn enormous thinking
2033
+ // budgets re-deriving context they cannot act on.
2034
+ thinking: this.planModeRef.current
2035
+ ? clampThinkingForPlanMode(this.thinkingLevel)
2036
+ : this.thinkingLevel,
2007
2037
  apiKey,
2008
2038
  // Per-turn credential resolution. A run can span many minutes; if any
2009
2039
  // process sharing auth.json refreshes this grant meanwhile, the token
@@ -2269,8 +2299,15 @@ export class AgentSession {
2269
2299
  await this.persistMessage(this.messages[i]);
2270
2300
  }
2271
2301
  this.lastPersistedIndex = this.messages.length;
2302
+ // Final response is delivered — compact in the background while the user
2303
+ // reads it, instead of charging the summarizer latency to their next
2304
+ // prompt. Detached by design; the pre-run path above remains the backstop.
2305
+ this.maybeCompactPostTurn(creds);
2272
2306
  }
2273
2307
  async switchModel(provider, model) {
2308
+ // The model-switch note is appended to `this.messages`; settle any
2309
+ // background compaction first so the note cannot be swapped out.
2310
+ await this.settlePostTurnCompaction();
2274
2311
  const prevProvider = this.provider;
2275
2312
  const prevModel = this.model;
2276
2313
  // Diff gate: a "switch" to the model already in use is not state change.
@@ -2471,6 +2508,101 @@ export class AgentSession {
2471
2508
  newCount: result.newCount,
2472
2509
  });
2473
2510
  }
2511
+ /**
2512
+ * Wait out any in-flight post-turn background compaction before a new
2513
+ * entry point mutates `this.messages` (prompt, attachments, model switch).
2514
+ * The background compact() snapshots and then REPLACES the array, so a
2515
+ * message pushed while it runs would be silently dropped from the live
2516
+ * history — the pre-run await in runLoop() sits after the push and cannot
2517
+ * protect it.
2518
+ */
2519
+ async settlePostTurnCompaction() {
2520
+ if (this.postTurnCompaction)
2521
+ await this.postTurnCompaction;
2522
+ }
2523
+ /**
2524
+ * Post-turn compaction: once the final response has been delivered, compact
2525
+ * in the background while the user reads the answer, instead of making the
2526
+ * next prompt pay the summarizer latency up front (the pre-run path stays as
2527
+ * the backstop, so a skipped or failed attempt costs nothing). Mirrors the
2528
+ * Codex `model_post_turn_compact_threshold_percent` guards: skip when user
2529
+ * input is already queued (it would race the next turn), when the run was
2530
+ * aborted, or during the failure cooldown — and never let a compaction
2531
+ * error surface in the completed turn.
2532
+ */
2533
+ maybeCompactPostTurn(creds) {
2534
+ if (!this.settingsManager.get("autoCompact"))
2535
+ return;
2536
+ if (this.opts.signal?.aborted)
2537
+ return;
2538
+ if (this.userQueue.length > 0)
2539
+ return;
2540
+ if (this.postTurnCompaction)
2541
+ return;
2542
+ if (Date.now() < this.compactionRetryAfter)
2543
+ return;
2544
+ // One compaction per turn boundary: a pre-run or overflow-recovery
2545
+ // compaction already shrank this run's history — re-probing right after
2546
+ // the final response would only re-derive that decision.
2547
+ if (this.compactionOccurred)
2548
+ return;
2549
+ const contextWindow = getContextWindow(this.model, {
2550
+ provider: this.provider,
2551
+ accountId: creds.accountId,
2552
+ });
2553
+ const policy = resolveCompactionPolicy({
2554
+ provider: this.provider,
2555
+ model: this.model,
2556
+ contextWindow,
2557
+ threshold: this.settingsManager.get("compactThreshold"),
2558
+ accountId: creds.accountId,
2559
+ approvedPlanPath: this.approvedPlanPath,
2560
+ });
2561
+ let activeTokens;
2562
+ if (this.providerContext) {
2563
+ const anchorIndex = this.messages.lastIndexOf(this.providerContext.anchor);
2564
+ if (anchorIndex >= 0) {
2565
+ activeTokens = calculateActiveContextTokens(this.messages, {
2566
+ usage: this.providerContext.usage,
2567
+ pendingMessages: this.messages.slice(anchorIndex + 1),
2568
+ });
2569
+ }
2570
+ }
2571
+ if (!shouldCompact(this.messages, contextWindow, policy.threshold, activeTokens))
2572
+ return;
2573
+ log("INFO", "compaction", "Post-turn compaction decision — compacting in background", {
2574
+ provider: this.provider,
2575
+ model: this.model,
2576
+ transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
2577
+ contextWindow: String(contextWindow),
2578
+ activeTokens: activeTokens === undefined ? "estimated" : String(activeTokens),
2579
+ triggerLimit: String(policy.targetTokens),
2580
+ });
2581
+ this.postTurnCompaction = (async () => {
2582
+ try {
2583
+ await this.compact(creds, "automatic");
2584
+ if (this.lastCompactionCompacted) {
2585
+ // Arm re-grounding for the next turn. resetHookState clears
2586
+ // compactionOccurred at run start, before the injection point, so
2587
+ // park the signal for pickup there.
2588
+ this.compactionArmedForNextRun = true;
2589
+ this.compactionRetryAfter = 0;
2590
+ }
2591
+ else {
2592
+ this.compactionRetryAfter = Date.now() + 30_000;
2593
+ }
2594
+ }
2595
+ catch (error) {
2596
+ this.compactionRetryAfter = Date.now() + 30_000;
2597
+ if (isAbortError(error) || this.opts.signal?.aborted)
2598
+ return;
2599
+ log("WARN", "compaction", `Post-turn compaction failed; cooling down for 30s: ${error instanceof Error ? error.message : String(error)}`);
2600
+ }
2601
+ finally {
2602
+ this.postTurnCompaction = undefined;
2603
+ }
2604
+ })();
2605
+ }
2474
2606
  async compact(existingCredentials, mode = "manual",
2475
2607
  /** User-stated focus (`/compact <focus>`): what must survive verbatim. */
2476
2608
  focus) {
@@ -3445,6 +3577,12 @@ export class AgentSession {
3445
3577
  return this.getPromptCacheKey();
3446
3578
  }
3447
3579
  async dispose() {
3580
+ // Quiesce any in-flight post-turn compaction BEFORE tearing down state:
3581
+ // the background compact() snapshots and replaces `this.messages`, so
3582
+ // letting it run past this point would checkpoint a near-empty history
3583
+ // and leak a junk session file after teardown.
3584
+ if (this.postTurnCompaction)
3585
+ await this.postTurnCompaction;
3448
3586
  this.diagnosticsRecorder?.finalize();
3449
3587
  this.managerAbortSignal?.removeEventListener("abort", this.managerAbortHandler);
3450
3588
  this.processManager?.shutdownAll();