Session Quality & Preemptive Rotation Fixes — Build Doc
========================================================

Problem
-------
Long-running Claude sessions (30+ hours) degrade: freezing mid-sentence, losing context, output quality dropping. GROOVE's rotation system triggers AFTER failure instead of BEFORE.

Root Cause
----------
Claude Code provider is flagged managesOwnContext = true (packages/daemon/src/providers/claude-code.js:40). This causes rotator.js line 183 to SKIP all context threshold, token ceiling, and hard ceiling checks. The only rotation path left is quality-based scoring, and it misses the specific degradation patterns.

Evidence from fullstack-10 (training team, 11-day session):
- 2.5M tokens, 247% context usage, ZERO quality rotations triggered
- All 7 rotations were natural_compaction or manual
- Output collapsed to 1-8 tokens per response (messages 1484-1510)
- Thinking blocks truncated mid-sentence at 8 tokens
- 5 consecutive identical python3 commands (agent stuck in loop)
- Cache read tokens dropped from 150K to 0 twice (context resets)
- 80 rate limit events
- Tool-call-only responses with zero explanatory text in late session


Files Modified
--------------

1. packages/daemon/src/process.js
   - Change 1: Incomplete response detection in _handleAgentOutput
   - Change 5: Cache reset detection (cacheReadTokens drop tracking)
   - Change 7: Intro context size warning after buildFullPrompt()

2. packages/daemon/src/rotator.js
   - Change 3: Compaction-aware rotation (compaction count tracking, age decay extension)
   - Change 4: Truncation-triggered immediate rotation
   - Change 8: Fix duplicate rotation history entries from recordNaturalCompaction()

3. packages/daemon/src/adaptive.js
   - Change 2: New signals in extractSignals() and scoreSession()

4. packages/daemon/src/journalist.js
   - Change 6: Handoff brief compression and deduplication


Change Details
--------------

CHANGE 1 — Incomplete Response Detection (process.js)

What: Detect mid-sentence truncation and abandoned tool calls.

Where: _handleAgentOutput method, triggered on output.type === 'activity' && output.subtype === 'assistant'.

Logic:
- Check if last text block ends without terminal punctuation (. ? ! or closing code fence/brace)
- Track if tool_use was emitted without a matching tool_result in next turn
- Set registry fields: truncationSuspected (bool), consecutiveTruncations (int)
- 2+ consecutive truncations = hard degradation signal
- Emit classifier event: { type: 'error', subtype: 'truncated_response' }
- Reduce stall threshold from 5min to 2min for truncation-flagged agents

Edge cases to watch:
- Short valid responses like "OK" or "Done" end with a period — should not trigger
- Tool-use-only responses (no text block at all) need special handling — check if it's a normal tool call or a degraded one by looking at whether the tool_use has meaningful input
- Must not trigger on the very first response of a session


CHANGE 2 — Output Velocity Decay Signals (adaptive.js)

What: Four new quality signals in extractSignals() with corresponding scoring in scoreSession().

New signals:
- outputLengthDecay: Compare avg output tokens in last 5 turns vs first 5. If <50%, signal=1. Score: -10 pts.
- toolOutputVolume: Sum tool result character lengths. >300KB = signal 1 (-5 pts). >600KB = signal 2 (-10 pts).
- turnLatencyTrend: Compare avg timestamp gaps in last 10 vs first 10 entries. If >2x, signal=1. Score: -5 pts.
- bashRepetition: Count consecutive identical Bash commands. 3+ identical = signal 1 (-8 pts).

Where in extractSignals(): After the existing fileChurn and errorTrend calculations.

Where in scoreSession(): After the existing errorTrend scoring block.

Edge cases to watch:
- outputLengthDecay needs enough turns to compare (require at least 10 entries with output tokens)
- bashRepetition should normalize commands (trim whitespace) before comparing
- toolOutputVolume counts cumulative session total, not per-turn


CHANGE 3 — Compaction-Aware Rotation (rotator.js)

What: Track compaction events per agent and use them to progressively tighten rotation criteria.

Where: 
- recordNaturalCompaction(): increment this.compactionCounts Map
- check(): new block BEFORE quality rotation (around line 240), applies to ALL providers
- scoreLiveSession(): extended age decay

Logic:
- this.compactionCounts = new Map() in constructor
- recordNaturalCompaction increments count for the agent
- In check(): if compactionCounts >= 5, force rotate with reason 'compaction_ceiling'
- In check(): if compactionCounts >= 3, use effectiveQualityThreshold = 55 instead of 40
- scoreLiveSession() extended age decay: >7200s=-15, >14400s=-20, >28800s=-25

Edge cases to watch:
- Compaction count must reset when agent is rotated (new agent ID = fresh count)
- Don't increment count for the duplicate "unknown" provider entries (see Change 8)
- The compaction ceiling check should respect MIN_AGE_SEC to avoid killing brand-new agents


CHANGE 4 — Truncation-Triggered Rotation (rotator.js)

What: Immediate rotation on consecutive truncated responses.

Where: check() method, BEFORE the quality rotation block.

Logic:
- Read agent.truncationSuspected and agent.consecutiveTruncations from registry
- consecutiveTruncations >= 2: force rotate, reason 'incomplete_response', bypass cooldown
- truncationSuspected true (single): lower effective quality threshold to 55

Edge cases to watch:
- Must still respect the singleTask skip for Codex
- Must still respect the idle check (don't rotate while agent is actively producing output)
- Clear truncation flags on the NEW agent after rotation


CHANGE 5 — Cache Reset Detection (process.js)

What: Detect sudden drops in cache read tokens as a context quality signal.

Where: _handleAgentOutput, near the existing contextUsage tracking block (around line 1383).

Logic:
- Add Map: this._prevCacheRead (agentId -> last cacheReadTokens value)
- On each output with cacheReadTokens: compare to previous value
- If previous > 50,000 AND current < previous * 0.5: cache reset detected
- Set registry field: cacheResetDetected = true
- Emit classifier event: { type: 'error', subtype: 'cache_reset' }
- Rotator treats cacheResetDetected same as truncationSuspected

Edge cases to watch:
- Cache builds up from 0 at session start — must require previous > 50K minimum
- Natural compaction causes a cache drop too, but recordNaturalCompaction already handles that — don't double-count
- If the provider doesn't report cacheReadTokens (non-Claude providers), skip this check


CHANGE 6 — Handoff Brief Compression (journalist.js)

What: Cap brief size and deduplicate content across rotations.

Where: generateHandoffBrief() method (line 851+) and buildRotationSynthesisPrompt() (line 436+).

Changes:
- Hard cap: 5000 chars total for the assembled brief. Truncate from the bottom (rotation history and original task get cut first).
- Replace inline discoveries with pointer to .groove/memory/agent-discoveries.jsonl
- Cap original task section to 500 chars
- Drop 'Recent User Messages' for quality_degradation rotations
- Reduce rotation history from 3 entries to 1
- buildRotationSynthesisPrompt: reduce input cap from 30K to 15K, response limit from 2000 to 1500
- Dedup: hash discovery entries and skip any that appear in the rotation history section

Priority order when truncating to fit 5000 char cap:
1. Unresolved Errors (keep)
2. User Constraints (keep)
3. Last 5 Tool Calls (keep)
4. Session Summary (truncate to 1500 chars if needed)
5. Rotation History (1 entry, truncate to 500 chars if needed)
6. Original Task (truncate to 500 chars)
7. Everything else (drop)


CHANGE 7 — Intro Context Size Warning (process.js or introducer.js)

What: Warn when injected context is too large, optionally truncate.

Where: After buildFullPrompt() in the spawn flow, or in the introducer where intro context is assembled.

Logic:
- Measure Buffer.byteLength(fullPrompt)
- If > 8000 chars: console.warn with agent name and size
- Config field: maxIntroContextChars (default 10000)
- If exceeded: truncate agent.introContext (not the task prompt) to fit
- Truncation should preserve the first and last sections, cutting middle content

Edge cases to watch:
- Don't truncate the task prompt — only the GROOVE-injected intro context
- The warning should include which agent and what size, so the user can debug


CHANGE 8 — Fix Duplicate Rotation History Entries (rotator.js)

What: recordNaturalCompaction() produces two entries per event — one real, one "unknown" with 0 tokens.

Where: recordNaturalCompaction() method (line 484+).

Evidence: rotation-history.json shows paired entries like:
  { agentName: "planner-9", provider: "claude-code", oldTokens: 4358, ... }
  { agentName: "planner-9", provider: "unknown", oldTokens: 0, ... }

Root cause: Likely the broadcast from recordNaturalCompaction triggers a listener that calls it again, or the provider lookup fails on the second invocation. Debug by checking what calls recordNaturalCompaction — it's called from process.js _handleAgentOutput (line 1392). Check if the same stdout event produces two parseOutput results.

Fix: Add a dedup guard — if the last entry in rotationHistory has the same agentId and a timestamp within 1 second, skip. Or fix the upstream double-call.


Testing
-------
- npm test from packages/daemon/ must pass with all changes
- npm run build from packages/gui/ must compile
- Manual test: spawn a long-running agent and verify compaction counting works
- Manual test: send a large file read and verify toolOutputVolume signal fires
- Verify rotation-history.json no longer produces duplicate entries

Rollback
--------
All changes are in 4 files in packages/daemon/src/. If any change causes issues:
- Revert the specific file to its pre-change state via git checkout
- Changes are independent enough that any single change can be reverted without affecting the others, EXCEPT:
  - Change 4 depends on Change 1 (truncation detection feeds truncation rotation)
  - Change 3 depends on recordNaturalCompaction (Change 8 touches the same method)
  - If reverting Change 8, also verify Change 3's compaction counting still works
