@prestyj/cli 5.19.0 → 5.21.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.
@@ -260,6 +260,15 @@ export declare class AgentSession {
260
260
  * only on a real edge. */
261
261
  private verificationArmed;
262
262
  private compactionOccurred;
263
+ /**
264
+ * Re-grounding carry-over for post-turn compaction. `resetHookState` clears
265
+ * `compactionOccurred` at run start — before the injection point — so a
266
+ * background compaction that finished after the final response parks its
267
+ * "re-ground next turn" signal here for pickup at the next run start.
268
+ */
269
+ private compactionArmedForNextRun;
270
+ /** In-flight post-turn (background) compaction, if any. */
271
+ private postTurnCompaction?;
263
272
  private lastCompactionCompacted;
264
273
  private compactionRetryAfter;
265
274
  /** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
@@ -574,6 +583,26 @@ export declare class AgentSession {
574
583
  /** Canonicalize a deferred restore before a new prompt can fork stale history. */
575
584
  private adoptDeferredCheckpointBeforePrompt;
576
585
  private persistCompactionCheckpoint;
586
+ /**
587
+ * Wait out any in-flight post-turn background compaction before a new
588
+ * entry point mutates `this.messages` (prompt, attachments, model switch).
589
+ * The background compact() snapshots and then REPLACES the array, so a
590
+ * message pushed while it runs would be silently dropped from the live
591
+ * history — the pre-run await in runLoop() sits after the push and cannot
592
+ * protect it.
593
+ */
594
+ private settlePostTurnCompaction;
595
+ /**
596
+ * Post-turn compaction: once the final response has been delivered, compact
597
+ * in the background while the user reads the answer, instead of making the
598
+ * next prompt pay the summarizer latency up front (the pre-run path stays as
599
+ * the backstop, so a skipped or failed attempt costs nothing). Mirrors the
600
+ * Codex `model_post_turn_compact_threshold_percent` guards: skip when user
601
+ * input is already queued (it would race the next turn), when the run was
602
+ * aborted, or during the failure cooldown — and never let a compaction
603
+ * error surface in the completed turn.
604
+ */
605
+ private maybeCompactPostTurn;
577
606
  compact(existingCredentials?: {
578
607
  accessToken: string;
579
608
  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. */
@@ -889,6 +899,7 @@ export class AgentSession {
889
899
  kind: "prompt",
890
900
  visibility: "transcript",
891
901
  }, options = {}) {
902
+ await this.settlePostTurnCompaction();
892
903
  await this.adoptDeferredCheckpointBeforePrompt();
893
904
  const slash = await this.resolveSlashInput(content);
894
905
  if (slash?.kind === "template") {
@@ -923,6 +934,7 @@ export class AgentSession {
923
934
  * attachments are always a direct conversational turn.
924
935
  */
925
936
  async promptWithAttachments(text, attachments) {
937
+ await this.settlePostTurnCompaction();
926
938
  await this.adoptDeferredCheckpointBeforePrompt();
927
939
  const parts = this.buildAttachmentParts(text, attachments);
928
940
  if (parts.length === 0)
@@ -1063,6 +1075,13 @@ export class AgentSession {
1063
1075
  this.backgroundVerification.delete(id);
1064
1076
  }
1065
1077
  this.compactionOccurred = false;
1078
+ // Post-turn compaction may have landed between runs — adopt its armed
1079
+ // signal so the re-grounding hook fires for this run exactly as it would
1080
+ // after a pre-run compaction.
1081
+ if (this.compactionArmedForNextRun) {
1082
+ this.compactionArmedForNextRun = false;
1083
+ this.compactionOccurred = true;
1084
+ }
1066
1085
  this.originalRequest = originalRequest;
1067
1086
  }
1068
1087
  /**
@@ -1930,6 +1949,11 @@ export class AgentSession {
1930
1949
  // of the public API model window. Failed/no-op attempts cool down across
1931
1950
  // prompts; provider overflow recovery still bypasses this path entirely.
1932
1951
  if (this.settingsManager.get("autoCompact") && Date.now() >= this.compactionRetryAfter) {
1952
+ // A post-turn compaction may still be running in the background. Let it
1953
+ // settle first: it usually already compacted this history, so the
1954
+ // decision below turns into a no-op instead of duplicate work.
1955
+ if (this.postTurnCompaction)
1956
+ await this.postTurnCompaction;
1933
1957
  const contextWindow = getContextWindow(this.model, {
1934
1958
  provider: this.provider,
1935
1959
  accountId: creds.accountId,
@@ -1997,7 +2021,13 @@ export class AgentSession {
1997
2021
  maxTokens: this.maxTokens,
1998
2022
  maxTurns: this.opts.maxTurns,
1999
2023
  maxTurnExtensions: this.opts.maxTurnExtensions,
2000
- thinking: this.thinkingLevel,
2024
+ // Plan mode caps effort at medium (Codex `plan_mode_reasoning_effort`
2025
+ // preset): read-only exploration doesn't need xhigh/max reasoning, and
2026
+ // deep-reasoning models left at the ceiling burn enormous thinking
2027
+ // budgets re-deriving context they cannot act on.
2028
+ thinking: this.planModeRef.current
2029
+ ? clampThinkingForPlanMode(this.thinkingLevel)
2030
+ : this.thinkingLevel,
2001
2031
  apiKey,
2002
2032
  // Per-turn credential resolution. A run can span many minutes; if any
2003
2033
  // process sharing auth.json refreshes this grant meanwhile, the token
@@ -2263,8 +2293,15 @@ export class AgentSession {
2263
2293
  await this.persistMessage(this.messages[i]);
2264
2294
  }
2265
2295
  this.lastPersistedIndex = this.messages.length;
2296
+ // Final response is delivered — compact in the background while the user
2297
+ // reads it, instead of charging the summarizer latency to their next
2298
+ // prompt. Detached by design; the pre-run path above remains the backstop.
2299
+ this.maybeCompactPostTurn(creds);
2266
2300
  }
2267
2301
  async switchModel(provider, model) {
2302
+ // The model-switch note is appended to `this.messages`; settle any
2303
+ // background compaction first so the note cannot be swapped out.
2304
+ await this.settlePostTurnCompaction();
2268
2305
  const prevProvider = this.provider;
2269
2306
  const prevModel = this.model;
2270
2307
  // Diff gate: a "switch" to the model already in use is not state change.
@@ -2465,6 +2502,101 @@ export class AgentSession {
2465
2502
  newCount: result.newCount,
2466
2503
  });
2467
2504
  }
2505
+ /**
2506
+ * Wait out any in-flight post-turn background compaction before a new
2507
+ * entry point mutates `this.messages` (prompt, attachments, model switch).
2508
+ * The background compact() snapshots and then REPLACES the array, so a
2509
+ * message pushed while it runs would be silently dropped from the live
2510
+ * history — the pre-run await in runLoop() sits after the push and cannot
2511
+ * protect it.
2512
+ */
2513
+ async settlePostTurnCompaction() {
2514
+ if (this.postTurnCompaction)
2515
+ await this.postTurnCompaction;
2516
+ }
2517
+ /**
2518
+ * Post-turn compaction: once the final response has been delivered, compact
2519
+ * in the background while the user reads the answer, instead of making the
2520
+ * next prompt pay the summarizer latency up front (the pre-run path stays as
2521
+ * the backstop, so a skipped or failed attempt costs nothing). Mirrors the
2522
+ * Codex `model_post_turn_compact_threshold_percent` guards: skip when user
2523
+ * input is already queued (it would race the next turn), when the run was
2524
+ * aborted, or during the failure cooldown — and never let a compaction
2525
+ * error surface in the completed turn.
2526
+ */
2527
+ maybeCompactPostTurn(creds) {
2528
+ if (!this.settingsManager.get("autoCompact"))
2529
+ return;
2530
+ if (this.opts.signal?.aborted)
2531
+ return;
2532
+ if (this.userQueue.length > 0)
2533
+ return;
2534
+ if (this.postTurnCompaction)
2535
+ return;
2536
+ if (Date.now() < this.compactionRetryAfter)
2537
+ return;
2538
+ // One compaction per turn boundary: a pre-run or overflow-recovery
2539
+ // compaction already shrank this run's history — re-probing right after
2540
+ // the final response would only re-derive that decision.
2541
+ if (this.compactionOccurred)
2542
+ return;
2543
+ const contextWindow = getContextWindow(this.model, {
2544
+ provider: this.provider,
2545
+ accountId: creds.accountId,
2546
+ });
2547
+ const policy = resolveCompactionPolicy({
2548
+ provider: this.provider,
2549
+ model: this.model,
2550
+ contextWindow,
2551
+ threshold: this.settingsManager.get("compactThreshold"),
2552
+ accountId: creds.accountId,
2553
+ approvedPlanPath: this.approvedPlanPath,
2554
+ });
2555
+ let activeTokens;
2556
+ if (this.providerContext) {
2557
+ const anchorIndex = this.messages.lastIndexOf(this.providerContext.anchor);
2558
+ if (anchorIndex >= 0) {
2559
+ activeTokens = calculateActiveContextTokens(this.messages, {
2560
+ usage: this.providerContext.usage,
2561
+ pendingMessages: this.messages.slice(anchorIndex + 1),
2562
+ });
2563
+ }
2564
+ }
2565
+ if (!shouldCompact(this.messages, contextWindow, policy.threshold, activeTokens))
2566
+ return;
2567
+ log("INFO", "compaction", "Post-turn compaction decision — compacting in background", {
2568
+ provider: this.provider,
2569
+ model: this.model,
2570
+ transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
2571
+ contextWindow: String(contextWindow),
2572
+ activeTokens: activeTokens === undefined ? "estimated" : String(activeTokens),
2573
+ triggerLimit: String(policy.targetTokens),
2574
+ });
2575
+ this.postTurnCompaction = (async () => {
2576
+ try {
2577
+ await this.compact(creds, "automatic");
2578
+ if (this.lastCompactionCompacted) {
2579
+ // Arm re-grounding for the next turn. resetHookState clears
2580
+ // compactionOccurred at run start, before the injection point, so
2581
+ // park the signal for pickup there.
2582
+ this.compactionArmedForNextRun = true;
2583
+ this.compactionRetryAfter = 0;
2584
+ }
2585
+ else {
2586
+ this.compactionRetryAfter = Date.now() + 30_000;
2587
+ }
2588
+ }
2589
+ catch (error) {
2590
+ this.compactionRetryAfter = Date.now() + 30_000;
2591
+ if (isAbortError(error) || this.opts.signal?.aborted)
2592
+ return;
2593
+ log("WARN", "compaction", `Post-turn compaction failed; cooling down for 30s: ${error instanceof Error ? error.message : String(error)}`);
2594
+ }
2595
+ finally {
2596
+ this.postTurnCompaction = undefined;
2597
+ }
2598
+ })();
2599
+ }
2468
2600
  async compact(existingCredentials, mode = "manual",
2469
2601
  /** User-stated focus (`/compact <focus>`): what must survive verbatim. */
2470
2602
  focus) {
@@ -3439,6 +3571,12 @@ export class AgentSession {
3439
3571
  return this.getPromptCacheKey();
3440
3572
  }
3441
3573
  async dispose() {
3574
+ // Quiesce any in-flight post-turn compaction BEFORE tearing down state:
3575
+ // the background compact() snapshots and replaces `this.messages`, so
3576
+ // letting it run past this point would checkpoint a near-empty history
3577
+ // and leak a junk session file after teardown.
3578
+ if (this.postTurnCompaction)
3579
+ await this.postTurnCompaction;
3442
3580
  this.diagnosticsRecorder?.finalize();
3443
3581
  this.managerAbortSignal?.removeEventListener("abort", this.managerAbortHandler);
3444
3582
  this.processManager?.shutdownAll();