feat(coding-agent): added async compaction and in-place handoff

- Added compaction.asyncEnabled (Async Compaction, default on): when
  context enters the pre-threshold band [threshold - lead, threshold)
  with lead = clamp(threshold * 0.125, 8192, 32000), maintenance
  speculatively summarizes in the background off a branch snapshot
  (first configured LLM-backed method: remote, handoff, or soft) using
  a side session id isolated from the live turn. Crossing the threshold
  splices the armed result in instantly instead of blocking on a
  summarization round-trip. Armed results are invalidated by branch
  changes, reset boundaries, model switches that strand provider-native
  replay payloads, and context growth past keepRecentTokens (which
  re-speculates); extensions registering session_before_compact keep
  exact blocking semantics (speculation disabled).
- Reworked handoff to commit in place: /handoff and the auto handoff
  method now write the generated document as a regular compaction entry
  on the current session (summary = document + <files> tag, cut from
  prepareCompaction) instead of starting a new session. SessionHandoff
  shrank to a document generator; session_before_switch/session_switch
  no longer fire with reason "handoff"; mid-turn maintenance no longer
  suppresses the handoff preference; overflow recovery can apply an
  armed handoff result.
- Extracted the shared auto-compaction commit tail
  (#commitAutoCompactionResult / #commitCompactionEntry) used by the
  blocking production path, the armed speculative apply, manual
  compaction, and manual handoff.
- Status line pulses the auto-compact icon while a speculation runs and
  holds it in accent once a result is armed.
- Exported remotePreserveReusable from pi-agent-core/compaction for
  apply-time validation of speculative remote results.
This commit is contained in:
can1357
2026-08-20 03:45:44 +02:00
parent eced7ab08a
commit 2f54e760c1
26 changed files with 1218 additions and 1043 deletions
@@ -324,6 +324,9 @@ export class StatusLineComponent implements Component {
#onBranchChange: (() => void) | null = null;
#disposed = false;
#autoCompactEnabled: boolean = true;
/** Pulse timer for the running-speculation indicator; live only while speculation runs. */
#speculationBlinkTimer: NodeJS.Timeout | undefined;
#speculationBlinkOn = true;
#hookStatuses: Map<string, string> = new Map();
#subagentCount: number = 0;
/**
@@ -694,12 +697,38 @@ export class StatusLineComponent implements Component {
this.#branchResolveActive = undefined;
this.#resetJjRequests();
this.#onBranchChange = null;
this.#stopSpeculationBlink();
this.#clearUsageStartTimer();
this.#onCodexResetFireworks = undefined;
this.#codexResetSnapshots.clear();
this.#retireGitWatcher();
}
/**
* Drive the context segment's pulse while a background speculative
* compaction runs: a slow toggle that invalidates and repaints through the
* same host callback async git/PR resolves use. Stops (and resets phase)
* the first render after speculation leaves the running state.
*/
#syncSpeculationBlink(state: "idle" | "running" | "armed"): void {
if (state === "running" && !this.#disposed) {
this.#speculationBlinkTimer ??= setInterval(() => {
this.#speculationBlinkOn = !this.#speculationBlinkOn;
this.invalidate();
this.#onBranchChange?.();
}, 600);
return;
}
this.#stopSpeculationBlink();
}
#stopSpeculationBlink(): void {
if (!this.#speculationBlinkTimer) return;
clearInterval(this.#speculationBlinkTimer);
this.#speculationBlinkTimer = undefined;
this.#speculationBlinkOn = true;
}
#clearUsageStartTimer(): void {
if (!this.#usageStartTimer) return;
clearTimeout(this.#usageStartTimer);
@@ -1599,6 +1628,8 @@ export class StatusLineComponent implements Component {
this.#getGitStatus(activeRepoCache.effectiveGitCwd))
: null;
const gitPr = includePr ? this.#lookupPr(activeRepoCache.effectiveGitCwd) : null;
const compactionSpeculation = this.session.compactionSpeculation ?? "idle";
this.#syncSpeculationBlink(compactionSpeculation);
return {
session: this.session,
focusedAgentId: this.#focusedAgentId,
@@ -1621,6 +1652,8 @@ export class StatusLineComponent implements Component {
contextTokens,
contextWindow,
autoCompactEnabled: this.#autoCompactEnabled,
compactionSpeculation,
speculationBlinkOn: this.#speculationBlinkOn,
subagentCount: this.#subagentCount,
activeMs: this.getActiveMs(),
git: {