From 2a7005f29aa9665e01e312ed877b009ac4c709de Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 22 Apr 2026 15:51:18 +0200 Subject: [PATCH] =?UTF-8?q?fix(dqn):=20Bug=20#2=20=E2=80=94=20epsilon=5Fgr?= =?UTF-8?q?eedy=5Faction=20samples=204-branch=20factored=20space=20(Task?= =?UTF-8?q?=202.5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stale 0..5 range from pre-2026-04-08 9-level code. Only test paths call this cold-path fallback, but the stale range was a latent foot-gun and would mislead anyone reading the code (per feedback_trust_code_not_docs). Fix: sample dir ∈ [0,3), mag ∈ [0,3), ord ∈ [0,3), urg ∈ [0,3) and encode as `dir*27 + mag*9 + ord*3 + urg`, matching MEMORY.md 4-branch DQN architecture (81 factored actions). Per Track 4 E1 triage tech-debt flag. --- crates/ml/src/trainers/dqn/trainer/action.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/ml/src/trainers/dqn/trainer/action.rs b/crates/ml/src/trainers/dqn/trainer/action.rs index 4db11fcd9..4e7171fbf 100644 --- a/crates/ml/src/trainers/dqn/trainer/action.rs +++ b/crates/ml/src/trainers/dqn/trainer/action.rs @@ -164,8 +164,18 @@ impl DQNTrainer { use rand::rngs::StdRng; let epsilon = self.get_epsilon().await? as f32; let mut rng = StdRng::seed_from_u64(0xAC7_DEF0); - if rng.gen::() < epsilon { Ok(rng.gen_range(0..5)) } - else { + if rng.gen::() < epsilon { + // Task 2.5 Bug #2: sample 4-branch factored action space + // (dir × mag × ord × urg = 3×3×3×3 = 81), matching MEMORY.md + // DQN architecture (updated 2026-04-08). The legacy 0..5 range + // predated the 4-branch refactor and was a latent foot-gun. + // Encoding: action = dir*27 + mag*9 + ord*3 + urg. + let dir = rng.gen_range(0..3usize); + let mag = rng.gen_range(0..3usize); + let ord = rng.gen_range(0..3usize); + let urg = rng.gen_range(0..3usize); + Ok(dir * 27 + mag * 9 + ord * 3 + urg) + } else { let agent = self.agent.read().await; let indices = agent.batch_greedy_actions(state)?; let best_action = *indices.first().ok_or_else(|| anyhow::anyhow!("argmax on empty Q-values"))? as usize;