@@ -99,36 +99,53 @@ function fnv1a(str: string): number {
9999
100100/**
101101 * Severity-aware codename pools.
102- * Each pool has [prefix[ ], core[]] tuples . The codename is formed as
102+ * Each pool has prefixes[ ], cores[], and epithets[] . The codename is formed as
103103 * "Prefix Core" by picking one from each using hash(monsterId).
104+ * On collision, a third word (epithet) is appended instead of a number.
104105 *
105106 * These pools are intentionally disjoint from agent name pools
106107 * (Nova, Forge, Atlas, Alpha, Beta, etc.).
107108 */
108- const CODENAME_POOLS : Record < string , { prefixes : readonly string [ ] ; cores : readonly string [ ] } > = {
109+ const CODENAME_POOLS : Record < string , {
110+ prefixes : readonly string [ ]
111+ cores : readonly string [ ]
112+ epithets : readonly string [ ]
113+ } > = {
109114 warning : {
110- prefixes : [ 'Grey' , 'Faint' , 'Pale' , 'Dusk' , 'Mist' , 'Haze' , 'Thin' , 'Dim' ] ,
111- cores : [ 'Wisp' , 'Moth' , 'Shade' , 'Flicker' , 'Murmur' , 'Ember' , 'Glint' , 'Ripple' ] ,
115+ prefixes : [ 'Grey' , 'Faint' , 'Pale' , 'Dusk' , 'Mist' , 'Haze' , 'Thin' , 'Dim' ,
116+ 'Wan' , 'Soft' , 'Low' , 'Still' , 'Cold' , 'Dry' , 'Slow' , 'Bare' ] ,
117+ cores : [ 'Wisp' , 'Moth' , 'Shade' , 'Flicker' , 'Murmur' , 'Ember' , 'Glint' , 'Ripple' ,
118+ 'Sigh' , 'Drift' , 'Hush' , 'Trace' , 'Echo' , 'Veil' , 'Frost' , 'Spark' ] ,
119+ epithets : [ 'Ashen' , 'Hollow' , 'Silent' , 'Fading' , 'Distant' , 'Waning' , 'Fleeting' , 'Sunken' ] ,
112120 } ,
113121 error : {
114- prefixes : [ 'Iron' , 'Rust' , 'Bitter' , 'Bleak' , 'Ashen' , 'Thorn' , 'Bane' , 'Grim' ] ,
115- cores : [ 'Fang' , 'Specter' , 'Wraith' , 'Vortex' , 'Rend' , 'Scourge' , 'Blight' , 'Fracture' ] ,
122+ prefixes : [ 'Iron' , 'Rust' , 'Bitter' , 'Bleak' , 'Ashen' , 'Thorn' , 'Bane' , 'Grim' ,
123+ 'Dusk' , 'Sable' , 'Keen' , 'Stark' , 'Nether' , 'Riven' , 'Wicked' , 'Fell' ] ,
124+ cores : [ 'Fang' , 'Specter' , 'Wraith' , 'Vortex' , 'Rend' , 'Scourge' , 'Blight' , 'Fracture' ,
125+ 'Maw' , 'Claw' , 'Shard' , 'Surge' , 'Rot' , 'Hex' , 'Torment' , 'Ruin' ] ,
126+ epithets : [ 'Risen' , 'Unbound' , 'Cursed' , 'Scarred' , 'Twisted' , 'Ragged' , 'Barbed' , 'Hollow' ] ,
116127 } ,
117128 critical : {
118- prefixes : [ 'Dire' , 'Dread' , 'Crimson' , 'Void' , 'Fell' , 'Wrath' , 'Doom' , 'Black' ] ,
119- cores : [ 'Hydra' , 'Leviathan' , 'Titan' , 'Colossus' , 'Behemoth' , 'Reaper' , 'Tyrant' , 'Inferno' ] ,
129+ prefixes : [ 'Dire' , 'Dread' , 'Crimson' , 'Void' , 'Fell' , 'Wrath' , 'Doom' , 'Black' ,
130+ 'Blood' , 'Shadow' , 'Storm' , 'Death' , 'Night' , 'Dark' , 'Chaos' , 'Bone' ] ,
131+ cores : [ 'Hydra' , 'Leviathan' , 'Titan' , 'Colossus' , 'Behemoth' , 'Reaper' , 'Tyrant' , 'Inferno' ,
132+ 'Warden' , 'Phantom' , 'Sovereign' , 'Herald' , 'Juggernaut' , 'Harbinger' , 'Sentinel' , 'Monolith' ] ,
133+ epithets : [ 'Eternal' , 'Ancient' , 'Unchained' , 'Supreme' , 'Undying' , 'Merciless' , 'Relentless' , 'Forsaken' ] ,
120134 } ,
121135 outage : {
122- prefixes : [ 'Abyssal' , 'Cataclysm' , 'Oblivion' , 'Ruin' , 'Eclipse' , 'Null' , 'Extinction' , 'Omega' ] ,
123- cores : [ 'Drake' , 'Kraken' , 'Annihilator' , 'Maelstrom' , 'Cataclysm' , 'Worldbreaker' , 'Tempest' , 'Ravager' ] ,
136+ prefixes : [ 'Abyssal' , 'Oblivion' , 'Ruin' , 'Eclipse' , 'Null' , 'Extinction' , 'Omega' , 'End' ,
137+ 'Nether' , 'Hollow' , 'Sundered' , 'Shattered' , 'Forsaken' , 'Eldritch' , 'Prime' , 'Apex' ] ,
138+ cores : [ 'Drake' , 'Kraken' , 'Annihilator' , 'Maelstrom' , 'Worldbreaker' , 'Tempest' , 'Ravager' , 'Scourge' ,
139+ 'Devourer' , 'Obliterator' , 'Cataclysm' , 'Dominion' , 'Nexus' , 'Terminus' , 'Sovereign' , 'Abyss' ] ,
140+ epithets : [ 'Ascended' , 'Absolute' , 'Infinite' , 'Ultimate' , 'Primordial' , 'Boundless' , 'Transcendent' , 'Final' ] ,
124141 } ,
125142}
126143
127144/** Default pool used when severity is unknown */
128145const DEFAULT_POOL = CODENAME_POOLS [ 'error' ] !
129146
130147/** Session-level codename deduplication registry */
131- const usedCodenames = new Map < string , number > ( )
148+ const usedCodenames = new Set < string > ( )
132149
133150/** Reset the dedup registry (call on scenario reload) */
134151export function resetErrorCodenames ( ) : void {
@@ -139,12 +156,15 @@ export function resetErrorCodenames(): void {
139156 * Generate a deterministic, human-friendly codename for an error/monster.
140157 *
141158 * Uses severity-aware word pools and FNV-1a hash of the monster ID for
142- * deterministic selection. Handles collisions within a session by appending
143- * a Roman numeral suffix (II, III, IV...).
159+ * deterministic selection. On collision, appends a third word (epithet)
160+ * from a dedicated pool — never digits or Roman numerals.
161+ *
162+ * With 16 prefixes x 16 cores = 256 base combinations per severity,
163+ * plus 8 epithets for collision expansion = 2048 total unique names.
144164 *
145165 * @param monsterId - Unique monster ID (required for determinism)
146166 * @param severity - Monster severity level (selects the word pool)
147- * @returns A codename like "Iron Specter" or "Dire Hydra II "
167+ * @returns A codename like "Iron Specter" or "Dire Hydra Eternal "
148168 */
149169export function generateErrorCodename ( monsterId : string , severity : string ) : string {
150170 const pool = CODENAME_POOLS [ severity ] ?? DEFAULT_POOL
@@ -155,16 +175,49 @@ export function generateErrorCodename(monsterId: string, severity: string): stri
155175 const core = pool . cores [ ( hash >>> 16 ) % pool . cores . length ] !
156176 const baseName = `${ prefix } ${ core } `
157177
158- // Dedup within session: if this exact codename was already issued, suffix it
159- const count = usedCodenames . get ( baseName ) ?? 0
160- usedCodenames . set ( baseName , count + 1 )
178+ // If base name is unique in this session, use it directly
179+ if ( ! usedCodenames . has ( baseName ) ) {
180+ usedCodenames . add ( baseName )
181+ return baseName
182+ }
183+
184+ // Collision: append an epithet chosen by a different hash mix
185+ const epithetHash = fnv1a ( monsterId + ':epithet' )
186+ const epithet = pool . epithets [ epithetHash % pool . epithets . length ] !
187+ const expanded = `${ baseName } ${ epithet } `
188+
189+ if ( ! usedCodenames . has ( expanded ) ) {
190+ usedCodenames . add ( expanded )
191+ return expanded
192+ }
161193
162- if ( count === 0 ) return baseName
194+ // Extremely rare double-collision: try remaining epithets deterministically
195+ for ( let i = 0 ; i < pool . epithets . length ; i ++ ) {
196+ const candidate = `${ baseName } ${ pool . epithets [ i ] ! } `
197+ if ( ! usedCodenames . has ( candidate ) ) {
198+ usedCodenames . add ( candidate )
199+ return candidate
200+ }
201+ }
202+
203+ // Exhausted all epithets: use a unique word-based suffix from the hash
204+ const fallback = `${ baseName } ${ hashWord ( monsterId ) } `
205+ usedCodenames . add ( fallback )
206+ return fallback
207+ }
163208
164- // Roman numeral suffixes for collisions (deterministic, readable)
165- const ROMAN = [ 'II' , 'III' , 'IV' , 'V' , 'VI' , 'VII' , 'VIII' , 'IX' , 'X' ]
166- const suffix = count <= ROMAN . length ? ROMAN [ count - 1 ] ! : `${ count + 1 } `
167- return `${ baseName } ${ suffix } `
209+ /** Generate a pronounceable word-like suffix from a hash (no digits) */
210+ function hashWord ( id : string ) : string {
211+ const h = fnv1a ( id + ':word' )
212+ const consonants = 'bcdfghjklmnprstvwz'
213+ const vowels = 'aeiou'
214+ // Build a 4-letter pronounceable token: CVCV
215+ const c1 = consonants [ h % consonants . length ] !
216+ const v1 = vowels [ ( h >>> 4 ) % vowels . length ] !
217+ const c2 = consonants [ ( h >>> 8 ) % consonants . length ] !
218+ const v2 = vowels [ ( h >>> 12 ) % vowels . length ] !
219+ const word = c1 + v1 + c2 + v2
220+ return word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 )
168221}
169222
170223// ---------------------------------------------------------------------------
@@ -220,9 +273,9 @@ function deriveDescriptor(
220273 const tool = errorDetails ?. tool_name
221274 const sig = extractSignature ( errorDetails ?. message )
222275
223- if ( tool && sig ) return ` ${ tool } ${ sig } `
276+ if ( tool && sig ) return tool + ': ' + sig
224277 if ( sig ) return sig
225- if ( tool ) return ` ${ tool } failed`
278+ if ( tool ) return tool + ' failure'
226279
227280 const excerpt = extractExcerpt ( errorDetails ?. message )
228281 if ( excerpt ) return excerpt
0 commit comments