@@ -10005,7 +10005,7 @@ def setup_hook(dry_run=False):
1000510005
1000610006# ========== Persistent Dashboard Daemon ==========
1000710007
10008- TOKEN_OPTIMIZER_VERSION = "5.9.2 " # Keep in sync with plugin.json + marketplace.json
10008+ TOKEN_OPTIMIZER_VERSION = "5.9.3 " # Keep in sync with plugin.json + marketplace.json
1000910009_DASHBOARD_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
1001010010_DAEMON_RUNTIME = detect_runtime()
1001110011_DAEMON_RUNTIME_SUFFIX = "codex" if _DAEMON_RUNTIME == "codex" else "claude"
@@ -17210,6 +17210,24 @@ def _maybe_progressive_checkpoint(fill_pct, cache_path, result, filepath):
1721017210_LOOP_SESSION_CAP = 2
1721117211_LOOP_LAST_MESSAGES = 4
1721217212
17213+ # Transient nudge/loop/warning state that must survive across the separate
17214+ # UserPromptSubmit and PostCompact hook processes. compute_quality_score() builds
17215+ # a fresh result each run, so quality_cache() carries these forward from the prior
17216+ # on-disk cache. Any new "_nudge_*"/"_loop_*"/warning-dedup key set in _maybe_nudge
17217+ # or _maybe_loop_warning MUST be registered here — a sibling key silently missing
17218+ # from this set is exactly what broke the nudge follow-through credit (A6).
17219+ _CARRY_KEYS = (
17220+ "_nudge_fill_pct_at_fire",
17221+ "_nudge_fire_epoch",
17222+ "_nudge_count",
17223+ "_nudge_last_epoch",
17224+ "_nudge_previous_score",
17225+ "_loop_warning_count",
17226+ "progressive_bands_captured",
17227+ "_last_fill_warn_level",
17228+ "_last_tool_call_warn_level",
17229+ )
17230+
1721317231
1721417232def _check_realtime_loops(quality_data):
1721517233 """Lightweight loop detection using already-parsed quality_data.
@@ -17565,10 +17583,7 @@ def quality_cache(throttle_seconds=120, warn_threshold=70, quiet=False, session_
1756517583
1756617584 _session_id = Path(cache_path).stem.replace("quality-cache-", "", 1) if cache_path else None
1756717585 result = compute_quality_score(quality_data, session_id=_session_id)
17568- for carry_key in ("_nudge_fill_pct_at_fire", "_nudge_count", "_nudge_last_epoch",
17569- "_nudge_previous_score", "_loop_warning_count",
17570- "progressive_bands_captured", "_last_fill_warn_level",
17571- "_last_tool_call_warn_level"):
17586+ for carry_key in _CARRY_KEYS:
1757217587 if carry_key in prev_result and carry_key not in result:
1757317588 result[carry_key] = prev_result[carry_key]
1757417589 result["total_messages"] = len(quality_data["messages"])
@@ -17687,33 +17702,53 @@ def quality_cache(throttle_seconds=120, warn_threshold=70, quiet=False, session_
1768717702 # Nudge follow-through: if PostCompact triggered this run (force=True)
1768817703 # and a nudge preceded the compact, measure the actual fill_pct recovery.
1768917704 if force and result.get("fill_pct", 0) > 0:
17690- nudge_fill = result.get("_nudge_fill_pct_at_fire", 0)
17691- if nudge_fill > 0:
17692- current_fill = result["fill_pct"]
17693- fill_delta = nudge_fill - current_fill
17694- ft_sid = Path(cache_path).stem.replace("quality-cache-", "", 1) if cache_path else None
17695- # A2 temporal gate: only credit a compaction that followed the nudge
17696- # within the window. A compaction long after the nudge was not its
17697- # follow-through, so it must not borrow the nudge's credit.
17698- fire_epoch = result.get("_nudge_fire_epoch", 0)
17699- within_window = fire_epoch and (time.time() - fire_epoch) <= _NUDGE_FOLLOWTHROUGH_WINDOW_SECONDS
17700- # A2 dedup: if a checkpoint_restore already credited this session's
17701- # recovery for this compaction (same SessionStart cycle), the nudge
17702- # must not double-credit the same tokens.
17703- already_credited = _checkpoint_restore_credited_recently(ft_sid)
17704- if fill_delta > 5 and within_window and not already_credited:
17705- context_size = detect_context_window()[0]
17706- measured_tokens_recovered = int(context_size * fill_delta / 100)
17707- _log_compression_event(
17708- feature="quality_nudge",
17709- original_text=" " * (measured_tokens_recovered * 4),
17710- compressed_text=f"nudge_followthrough:fill={nudge_fill}->{current_fill}",
17711- session_id=ft_sid,
17712- detail=f"measured_recovery: fill {nudge_fill}%->{current_fill}% = {measured_tokens_recovered} tokens on {context_size} context",
17713- verified=True,
17714- )
17715- result.pop("_nudge_fill_pct_at_fire", None)
17716- result.pop("_nudge_fire_epoch", None)
17705+ # A6: the nudge fires during a prior UserPromptSubmit process; this
17706+ # follow-through runs in a SEPARATE PostCompact process. Fire-state reaches
17707+ # us via the atomic per-session quality cache (carried forward at the top of
17708+ # this function). Fall back to prev_result directly in case the carry did not
17709+ # run, so a heeded session is never misclassified as ignored. Wrapped so a
17710+ # corrupt cache value can never crash the host hook — it degrades to "no
17711+ # credit", not a traceback.
17712+ try:
17713+ nudge_fill = result.get("_nudge_fill_pct_at_fire") or prev_result.get("_nudge_fill_pct_at_fire", 0)
17714+ if nudge_fill > 0:
17715+ current_fill = result["fill_pct"]
17716+ fill_delta = nudge_fill - current_fill
17717+ ft_sid = Path(cache_path).stem.replace("quality-cache-", "", 1) if cache_path else None
17718+ # A2 temporal gate: only credit a compaction that followed the nudge
17719+ # within the window. A compaction long after the nudge was not its
17720+ # follow-through, so it must not borrow the nudge's credit.
17721+ fire_epoch = result.get("_nudge_fire_epoch") or prev_result.get("_nudge_fire_epoch", 0)
17722+ within_window = fire_epoch and (time.time() - fire_epoch) <= _NUDGE_FOLLOWTHROUGH_WINDOW_SECONDS
17723+ # A2 dedup: if a checkpoint_restore already credited this session's
17724+ # recovery for this compaction (same SessionStart cycle), the nudge
17725+ # must not double-credit the same tokens.
17726+ already_credited = _checkpoint_restore_credited_recently(ft_sid)
17727+ should_credit = fill_delta > 5 and within_window and not already_credited
17728+ # Consume the fire-state FIRST, then credit only if the consume was
17729+ # actually persisted. The fire-state lives in exactly one place (this
17730+ # per-session cache), so once it is cleared from disk no later
17731+ # PostCompact can re-enter this block for the same nudge — that makes
17732+ # the credit idempotent without a second DB query. If the process
17733+ # dies between here and the credit, or the clear-write fails, we skip
17734+ # the credit (a conservative under-count) rather than risk a
17735+ # double-count that would inflate the savings number.
17736+ result.pop("_nudge_fill_pct_at_fire", None)
17737+ result.pop("_nudge_fire_epoch", None)
17738+ consumed = _write_quality_cache(cache_path, result)
17739+ if should_credit and consumed:
17740+ context_size = detect_context_window()[0]
17741+ measured_tokens_recovered = int(context_size * fill_delta / 100)
17742+ _log_compression_event(
17743+ feature="quality_nudge",
17744+ original_text=" " * (measured_tokens_recovered * 4),
17745+ compressed_text=f"nudge_followthrough:fill={nudge_fill}->{current_fill}",
17746+ session_id=ft_sid,
17747+ detail=f"measured_recovery: fill {nudge_fill}%->{current_fill}% = {measured_tokens_recovered} tokens on {context_size} context",
17748+ verified=True,
17749+ )
17750+ except Exception:
17751+ pass
1771717752
1771817753 # Progressive checkpoints (v3.0)
1771917754 if _PROGRESSIVE_ENABLED and result.get("fill_pct", 0) > 0:
0 commit comments