ruvnet/RuView · error · Error
duplicate brain id: ${record.id}
Error message
duplicate brain id: ${record.id} What it means
Raised at auth.py:265 after a token passes signature and expiry verification but its decoded payload has no 'sub' claim (or sub is empty/None). The middleware requires sub to carry the username it will look up in UserManager.
Source
Thrown at harness/homecore/src/brain.js:91
if (Buffer.byteLength(raw) > 1_048_576) throw new Error('brain corpus exceeds 1 MiB');
const records = raw.split('\n').filter(Boolean).map((line, index) => {
if (Buffer.byteLength(line) > 16_384) {
throw new Error(`brain line ${index + 1}: exceeds 16 KiB`);
}
let record;
try {
record = JSON.parse(line);
} catch (error) {
throw new Error(`brain line ${index + 1}: ${error.message}`);
}
const errors = validateBrainRecord(record, { canonical: true });
if (errors.length) throw new Error(`brain line ${index + 1}: ${errors.join('; ')}`);
return Object.freeze(record);
});
if (records.length > 1000) throw new Error('brain corpus exceeds 1000 records');
const ids = new Set();
for (const record of records) {
if (ids.has(record.id)) throw new Error(`duplicate brain id: ${record.id}`);
ids.add(record.id);
}
return { records, digest: sha256(raw), bytes: Buffer.byteLength(raw) };
}
function terms(value) {
return new Set(String(value).toLowerCase().match(/[a-z0-9][a-z0-9_-]{1,}/g) || []);
}
export function searchBrain(query, { limit = 8, path = CORPUS_PATH } = {}) {
const wanted = terms(query);
if (!wanted.size) return [];
const { records, digest } = loadBrain(path);
return records.map((record) => {
const title = terms(record.title);
const body = terms(record.content);
const tags = new Set(record.tags.map((tag) => tag.toLowerCase()));
let score = 0;View on GitHub (pinned to 4685618388)
Solutions
- Always include 'sub': username when building token_data before create_access_token
- If consuming foreign tokens, map their subject claim to sub before verification or extend the lookup to the actual claim name
- Validate issued tokens with jwt.io / decode_token_claims to confirm sub is present
Example fix
# before
access_token = token_manager.create_access_token({"email": email})
# after
access_token = token_manager.create_access_token({"sub": username, "email": email}) Defensive patterns
Strategy: validation
Validate before calling
def token_data_is_valid(token_data: dict) -> bool:
"""create_access_token payload must carry a non-empty sub for _authenticate_request."""
return bool(token_data.get("sub")) Type guard
def has_subject_claim(claims: dict) -> bool:
return isinstance(claims.get("sub"), str) and len(claims["sub"]) > 0 Try / catch
try:
user_info = await middleware._authenticate_request(request)
except AuthenticationError as e:
if str(e) == "Invalid token payload":
# token verified but lacks sub -> re-issue with sub at the token mint
return json_response({"error": "token missing sub claim; re-issue required"}, 401)
raise Prevention
- Always set sub when minting tokens
- Decode new tokens with decode_token_claims in tests and assert sub exists
- Document 'sub = username' as the token contract
When it happens
Trigger: Calling TokenManager.create_access_token({'email': ...}) without a 'sub' key; tokens minted by a different service that uses 'username' or 'user_id' as the subject claim instead of 'sub'; payload.get('sub') returning '' because an empty string was set.
Common situations: Custom token issuance code that forgets the sub claim; integrating third-party-issued JWTs whose claim names differ; test fixtures hand-crafting payloads with only exp/iat.
Related errors
- guidance repoRoot must be a string or null
- brain line ${index + 1}: exceeds 16 KiB
- brain line ${index + 1}: ${error.message}
- brain line ${index + 1}: ${errors.join('; ')}
- brain corpus exceeds 1000 records
AI-assisted analysis of ruvnet/RuView@4685618388 (2026-08-16).
Data as JSON: /api/errors/ea906a03753b6128.
Report an issue: GitHub.