#19401: fix(ui): prevent precision loss when coercing large numeric strings in config form
app: web-ui
size: S
trusted-contributor
Cluster:
Discord and MS Teams Fixes
## Problem
When users edit config via the Control UI form mode, the form coercion logic silently corrupts Discord Snowflake IDs (and other large numeric strings).
**Example:**
- Input: `"805422583936421918"` (valid string snowflake)
- Output: `805422583936421900` (corrupted number with precision loss)
This breaks Discord DMs because the snowflake ID no longer matches.
## Root Cause
The `coerceFormValues()` function in `ui/src/ui/controllers/config/form-coerce.ts` handles `anyOf/oneOf` schemas by trying to coerce strings to numbers when a number variant exists.
Discord IDs are defined as `z.union([z.string(), z.number()])` which becomes a JSON Schema `anyOf` with string and number variants.
When coercing `"805422583936421918"`:
1. Loop finds the `number` variant
2. `Number("805422583936421918")` → `805422583936421900` (JavaScript loses precision on 64-bit integers)
3. Returns the corrupted number instead of preserving the valid string
## Fix
Before returning a coerced number, check if round-tripping preserves the value:
```typescript
if (String(parsed) !== trimmed) {
return value; // Keep original string — precision would be lost
}
```
This ensures:
- Large numeric strings (snowflakes, etc.) are preserved as strings
- Small numbers that fit safely are still coerced to numbers for correct typing
## Testing
Added test cases for:
- Preserving large numeric strings (Discord snowflake) in anyOf union
- Still coercing small numeric strings when precision is preserved
Verified manually:
```javascript
// Before fix:
coerceFormValues({allowFrom: ["805422583936421918"]}, schema)
// → {allowFrom: [805422583936421900]} // WRONG
// After fix:
coerceFormValues({allowFrom: ["805422583936421918"]}, schema)
// → {allowFrom: ["805422583936421918"]} // CORRECT
```
Fixes #17802
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR fixes a real precision-loss bug where Discord Snowflake IDs (and other 64-bit integers) were being silently corrupted when coerced through `Number()` in the config form. The root cause and approach are correct in intent.
However, the precision guard introduced in `coerceNumberString` — `String(parsed) !== trimmed` — is overly broad. It fires correctly for large integers beyond `Number.MAX_SAFE_INTEGER` (e.g. `"805422583936421918"` → `"805422583936421900"`), but it also fires for lossless float normalizations like `"1.0"` → `1` (where `String(1)` is `"1"`, not `"1.0"`). This breaks the existing test case that coerces `"1.0"` to the number `1` in a `type: "number"` field.
**Key issues:**
- The `String(parsed) !== trimmed` check will cause the existing `"coerces string numbers to numbers based on schema"` test to fail for the `"1.0"` input, since `String(Number("1.0"))` is `"1"` not `"1.0"`.
- The correct guard should target only the unsafe-integer case: `Number.isInteger(parsed) && !Number.isSafeInteger(parsed)`, which precisely catches 64-bit integer overflow without affecting float string normalization.
- The two new test cases are well-constructed and do verify the intended behavior.
<h3>Confidence Score: 2/5</h3>
- This PR introduces a regression: the `String(parsed) !== trimmed` guard breaks coercion of valid float strings like `"1.0"` to numbers, which fails an existing test.
- The fix correctly identifies the snowflake precision-loss problem and the overall approach is sound, but the chosen guard condition is too broad. It catches lossless representations like `"1.0"` → `1` as false positives, causing at minimum one existing test to fail (`cost.output` expected to be `1` from input `"1.0"`). The fix needs a targeted guard (`Number.isInteger(parsed) && !Number.isSafeInteger(parsed)`) before it can be safely merged.
- `ui/src/ui/controllers/config/form-coerce.ts` lines 30–32 — the round-trip guard needs to be narrowed to only block unsafe integers.
<sub>Last reviewed commit: f9df4e3</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#22557: fix(discord): coerce exec approval approver IDs to string to preven...
by zwffff · 2026-02-21
81.1%
#22524: fix(doctor): preserve precision of large Discord snowflake IDs in -...
by jmasson · 2026-02-21
78.2%
#19111: fix(config): warn on numeric IDs that lose precision
by Clawborn · 2026-02-17
77.5%
#16828: fix(config): transform Discord user/role IDs to strings
by Limitless2023 · 2026-02-15
77.2%
#10807: fix(config): coerce numeric meta.lastTouchedAt to ISO string
by mcaxtr · 2026-02-07
76.2%
#12204: fix(discord): resolve numeric guildId/channelId pairs in channel al...
by mcaxtr · 2026-02-09
74.6%
#12792: fix: exclude 'tokens' (plural) fields from config redaction
by jpaine · 2026-02-09
73.1%
#21463: fix(discord): prevent WebSocket death spiral + fix numeric channel ID…
by akropp · 2026-02-20
73.0%
#17380: fix(imessage): reject non-numeric chat_id values to prevent silent ...
by aldoeliacim · 2026-02-15
72.8%
#13960: fix(ui): preserve structured config validation error details
by constansino · 2026-02-11
72.5%