Skip to content

Epic: Stabilize per-turn extraction token demand at quality parity (bounded growth over 300+ turns) #477

Description

@daviburg

Epic: Stabilize per-turn extraction token demand at quality parity

Ultimate goal: Per-turn extraction token demand must STABILIZE as a session grows to 300+ turns (NOT grow unbounded) WHILE preserving near-ideal semantic extraction quality.

This is the permanent tracker for the per-turn extraction token-stabilization work over the coming weeks. All findings below were established from a 344-turn run (eval-qwen36-344t-full), re-derived from code + logs by the token-economist and quality-analyst specialists.

Predecessor / lineage: #385 (Context budget for relationship mapper and entity detail). Related: #447 (strategic-redirection epic), #460 and #468 (mistargeted compression -- see Decision below).


Root cause: the runaway

  • Per-turn total input tokens grow LINEARLY UNBOUNDED: approximately 15182 + 88.6 * turn_index, with NO plateau (~16K at turn 30 -> ~42K at turn 300 -> ~46K at turn 344).
  • There is NO rolling transcript/history window -- each LLM phase uses only the current turn's text. History is NOT the driver.
  • DRIVER = the entity_detail phase (64% of load, ~9.8K -> ~27.8K/turn):
    1. Detail-call count rises 3.25 -> 5.93/turn toward the cap _MAX_DETAIL_ENTITIES_PER_TURN=6 (tools/semantic_extraction.py ~L1161).
    2. The PC (player character) prior-state is UNCAPPED -- _format_prior_entity_context (~L1500-1610) caps non-PC relationships at _SCENE_MAX_RELATIONSHIPS=8, but the player keeps ALL relationships (109 at turn 344, 100% active, 0 ever resolved), and the PC appears in nearly every turn.
    3. ~15-17K tokens/turn of PURE REPETITION -- the system template + full turn text is re-sent across ~8.9 LLM calls/turn at scale.
  • Secondary: relationship_mapper grows 2.3K -> 7K/turn (near its 0.2 * ctx budget).

This epic CONTINUES #385, which capped NON-PC entity detail + the relationship mapper but LEFT THE PC PATH UNCAPPED. That gap is the dominant remaining driver.


Decision on #460 / PR #468

The "adaptive pressure-gated context compression" (#460 / PR #468) is MISTARGETED:

  • It gates only the known-entities catalog block (~13% of load, dormant in the A/B, <4% savings even if engaged).
  • It is latently quality-risky: the catalog block is the coreference anchor; dropping an entry causes the entity to be re-extracted as a NEW duplicate.

Resolution:


Levers (checklist, ranked by quality-safety)

A deep data-driven design report is being authored before implementation; it will refine L1/L2 scoping. Each lever is evaluated independently.

  • L1 -- Type-tiered PC relationship cap (SHIP-FIRST, low risk). Keep permanent bonds uncapped (kinship / adversarial / mentorship / political / captor-debt); recency + mention-filter the volatile spatial/social tail (~cap 10); force-keep anything mentioned this turn. PC web 109 -> ~30-40. MUST tier by relationship TYPE (0 rels ever resolve, so recency/status is an unreliable freshness signal). Needs a thin callback-retention A/B.
  • L4 -- relationship_mapper budget reuses L1 type-tiering (ships with L1, low risk / low yield).
  • L2 -- Batch entity_detail calls to kill the ~15K/turn template + turn-text REPETITION (BIGGEST token win). Risk: cross-entity attribute bleed -> keep PC + new + high-confidence SOLO, batch only the low-salience existing tail 2-3/call with hard-delimited per-entity blocks. NEEDS an A/B before trust.
  • L3 -- Do NOT lower the detail cap (rejected as a token lever). It only bites on busy high-info turns where dropping a detail pass is most costly; 525 updates were already dropped at cap=6. Instead FIX entity SELECTION: prioritize by state-change-this-turn + mention-salience + staleness, not discovery-time confidence.

Deliverables

  • Salvage feat: adaptive pressure-gated context compression #468 instrumentation into an instrumentation-only PR (orchestrator task queued).
  • Deep data-driven design report (per-phase token breakdown + per-lever target mapping + honest expected-return estimates) -- being authored.
  • Add a token-budget / lever-analysis visualization panel to the Extraction KPI Dashboard (private repo saas/orchestrator/dashboard.py) as a permanent reference of the initial ESTIMATES to compare against later MEASURED impact.
  • Fix the KPI dashboard baseline: the headline baseline is the 100-turn slice (eval-qwen36-100t-temp0-clean), which UNDERSTATES the runaway (only visible at scale). Promote the 344-turn full run (eval-qwen36-344t-full) as the meaningful baseline + add a token-growth-trend KPI.
  • Implement levers L1 + L4, then L2 (each with the standard A/B per docs/ab-test-standard.md), measuring actual token reduction vs the dashboard estimates, at quality parity (entity-set retention, no new duplicates / phantoms).

Success criteria

Per-turn token demand flattens (asymptotically bounded, not linearly growing) across a 300+ turn session, with extraction quality at parity (no entity drops, no new duplicates, no attribute corruption) vs the 344-turn baseline (eval-qwen36-344t-full).


Notes


Lessons learned + current state (consolidated) -- updated 2026-06-11

Authored by @pm after the A/B campaign on eval-qwen36-344t-full. This section is the durable record. Future analyses MUST build on it, not re-derive from the older design docs. Grounded on origin/main f85b665 + measured logs. Replaces the lever-by-lever checklist above as the current-truth status.

The 7 consolidated facts (verified)

  1. L1 relationship_type_tiering (feat: type-tiered PC relationship cap (L1+L4, config-gated default OFF) #480/feat: enable relationship_type_tiering by default #481) is MERGED + default-ON. PC permanent-bond types (kinship/adversarial/mentorship/political/partnership) are UNCAPPED; the volatile tail is capped at 10; non-PC capped 8. This OBSOLETES the "uncapped PC web" premise of design PRs docs: per-turn token-stabilization design report #478/docs: bounded per-turn context architecture (A1+A2) design report #479.
  2. A0 checkpoint-compaction (feat: A0 periodic checkpoint-compaction digest body (config-gated, default OFF) [epic #477] #482/feat: A0 periodic checkpoint-compaction digest body (#482) #483) = NULL / ICED. It compressed the volatile block, which a downstream max_snapshots=3 cap ALREADY bounded -> ~0 token lever. Its A/B was measurement-blind (it scored uncompacted raw the feature never touched, fix: entity_detail prompt-token instrumentation is blind to compaction (raw_tokens captured post-compression) #484). PR feat: A0 periodic checkpoint-compaction digest body (#482) #483 closed unmerged; branch feat/issue-482-a0-checkpoint-compaction PRESERVED (substrate for lever c-ii / feat: checkpoint context reset/rebuild with identity-anchor floor (epic #477, lever c-ii) #496).
  3. L2 batch-entity_detail (feat: batch entity_detail calls to eliminate per-call template+turn-text repetition (L2) #491/feat: batch entity_detail calls to collapse template+turn duplication (L2, #491) #493) = MERGED default-OFF, WITHIN-NOISE as shipped. conf>=0.8 tiering left only 0.01 entities/turn batchable (91.5% of entities sit at conf0.9). No quality harm, no token win at the shipped config. N=3 paired verdict NOT SEPARABLE from noise. It is a throughput/wall-clock enabler, not a per-turn bounding lever.
  4. A1a anchor-floor (feat: identity-anchor floor + checkpoint digest body (A1a) -- bound catalog-coupled context growth #492) = DEAD PREMISE. The identity/relationship body it targeted is now bounded by L1; what remains (identity + stable ~190 tok) is STATIC -> A1a is a pure constant downshift (slope unchanged) plus a quality risk (merge needs identity/stable visible). Its still-valid anchor-floor idea is FOLDED into lever c-ii (feat: checkpoint context reset/rebuild with identity-anchor floor (epic #477, lever c-ii) #496).
  5. DECISIVE STRUCTURAL FINDING. Per-turn token growth = calls/turn (3.2 -> 5.7, cap-6 SATURATED, ~290-525 updates/run silently dropped) x (template 2175 fixed + turn_text 212 -> 972 + per-entity prior ~flat) + PC permanent web (uncapped). Late-window slope ~151 tok/turn, NO plateau; segmentation (auto-warned at 100 turns) does NOT reset injected context. CONCLUSION: NO per-turn COMPACTION lever can asymptotically BOUND the curve -- compaction multiplies each term by a constant < 1; it cannot change the COUNT of terms nor the linear growth of the carried web. Only de-dup (flatten) or constant-trim. Bounding requires attacking call MULTIPLICITY (relevance-scoped detail = lever b / feat: relevance-scoped entity_detail selection (A2a) -- bound calls/turn (epic #477, lever b) #494) and ACCUMULATION (cap PC web = c-i / feat: cap+age PC permanent-bond web -- extend L1 type-tiering (epic #477, lever c-i) #495; checkpoint reset = c-ii / feat: checkpoint context reset/rebuild with identity-anchor floor (epic #477, lever c-ii) #496).
  6. COST FACT. GPU wall-clock is 86.8% DECODE / 13.2% prefill; llama.cpp single-slot prefix-cache already reuses duplicated input. So input-token reduction (option a / L2) gives ~0-2% real speedup -- NOT an A/B-cost lever. What cuts A/B cost = fewer GENERATING calls = lever (b). (b) is the DUAL lever: it bounds the curve AND cheapens every future A/B.
  7. METHODOLOGY. The A/B is non-deterministic at 344-turn depth (noise floor ~5-13 tok/call). Use feat: paired multi-run A/B scoring on the entity_detail token metric (#487) #488 paired multi-run (>=3/variant) + per-turn scoring. Every future lever must pass TWO pre-checks BEFORE GPU spend: (i) projected effect EXCEEDS the noise floor, and (ii) it actually FLATTENS the curve (late-window slope -> 0), not merely beats the baseline by X%.

Lever scoreboard

Lever Issue(s) Status Verdict
L1 type-tiered PC rel cap #480/#481 MERGED, default-ON WIN (bounds volatile tail)
L4 rel_mapper reuses L1 #480 MERGED WIN (rides L1)
A0 checkpoint-compaction #482/#483 ICED / NULL, branch preserved ~0 lever; substrate feeds c-ii
L2 batch entity_detail (option a) #491/#493 MERGED, default-OFF WITHIN-NOISE as shipped; throughput enabler only
A1a anchor-floor #492 DEAD premise; folded into c-ii superseded by L1 + #496
(b) relevance-scoped detail (A2a) #494 NEW -- FIRST bounds call multiplicity; the bounding + A/B-cost lever
(c-i) PC permanent-web cap+age #495 NEW bounds carried PC web (parallel-safe with #494)
(c-ii) checkpoint reset/rebuild #496 NEW -- LAST bounds carried context to a floor

The bounding path = (b) + (c). Compaction (A0/L2/A1a) is closed as a bounding strategy. Dependency order: #494 (S0 shadow -> S1 gate) FIRST; #495 parallel-safe with #494's S1; #496 LAST (highest coreference risk). Design: docs/design-bounded-context-relevance-reset.md.

STANDING RULE (applies to every agent, every future analysis)

  1. Ground in current origin/main + measured logs, NOT design docs. git fetch first. Treat docs: per-turn token-stabilization design report #478/docs: bounded per-turn context architecture (A1+A2) design report #479 (and the A1a feat: identity-anchor floor + checkpoint digest body (A1a) -- bound catalog-coupled context growth #492 framing) as SUPERSEDED by docs/design-bounded-context-relevance-reset.md wherever they conflict. The specific stale premise is the pre-L1 "uncapped PC web".
  2. TWO pre-checks before ANY GPU spend: (i) effect > feat: paired multi-run A/B scoring on the entity_detail token metric (#487) #488 noise floor, (ii) late-window slope -> 0 (the asymptotic-bound test, distinct from beating the baseline). A positive-slope result fails the bounding claim regardless of intercept improvement.
  3. Non-blind by construction. Every lever must move a per-turn field the feat: paired multi-run A/B scoring on the entity_detail token metric (#487) #488 paired scorer reads (the A0 fix: entity_detail prompt-token instrumentation is blind to compaction (raw_tokens captured post-compression) #484 lesson). No A/B against a field the feature does not touch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions