Knights and Knaves and Codes
Zero Information Bot
function ZeroInformationBot(people, bots, data) {
    if (!data) {
        data = {
            botData: [],
            knaves: [],
            guess: &quot;&quot;,
            newKnaves: false,
            attestations: Array(36).fill(0),
            threshold: -5,
        };
    }
    let newResponses = [];
    readResponses().forEach(res =&gt; {
        if (!data.botData[res.uid][res.index]){
            newResponses.push(res);
            data.botData[res.uid][res.index] = res.char;
        }
    });
    newResponses.forEach(res =&gt; {
        if (people.find(p =&gt; (p[0].index == res.index &amp;&amp; p[0].char == res.char) || (p[1].index == res.index &amp;&amp; p[1].char == res.char))){
            people.forEach((p, i) =&gt; {
                if ((p[0].index == res.index &amp;&amp; p[0].char == res.char) || (p[1].index == res.index &amp;&amp; p[1].char == res.char)) data.attestations[i] += 2;
                if ((p[0].index == res.index &amp;&amp; p[0].char != res.char) || (p[1].index == res.index &amp;&amp; p[1].char != res.char)) data.attestations[i] -= 1;
                if (data.attestations[i] &lt; -1 &amp;&amp; !data.knaves.find(i)) {
                    data.knaves.push(i);
                    data.newKnaves = true;
                }
            });
        }
    });
    viewRequests().forEach(req =&gt; {
        if (!data.botData[req.uid]){
            data.botData[req.uid] = Array(36).fill([]);
        } else if (!data.botData[req.uid][req.index]) {
            sendRequest(req.uid, req.index);
            data.botData[req.index] = &quot;-&quot;
        } else if (data.botData[req.uid][req.index] != &quot;-&quot;) {
            replyRequest(req.uid, req.index, data.botData[req.uid][req.index]);
        }
    });
    if (!data.newKnaves &amp;&amp; data.guess) return data.guess;
    data.newKnaves = false;
    let detected = true;
    let loop = false;
    let possibilities;
    while (detected){
        detected = false;
        possibilities = Array(36).fill(new Set());
        let examined = [], chargroups = [];
        people.forEach((p, i) =&gt; {
            if (data.knaves.find(i)) return;
            possibilities[p[0].index].add(p[0].char);
            possibilities[p[1].index].add(p[1].char);
            let first = examined.find(e =&gt; e.index == p[0].index &amp;&amp; e.char == p[0].char);
            let second = examined.find(e =&gt; e.index == p[1].index &amp;&amp; e.char == p[1].char);
            if (first &amp;&amp; !second){
                chargroups[first.group].push(p[1]);
                examined.push({index: p[1].index, char: p[1].char, group: first.group});
            } else if (!first &amp;&amp; second){
                chargroups[second.group].push(p[0]);
                examined.push({index: p[0].index, char: p[0].char, group: second.group});
            } else if (!first &amp;&amp; !second){
                examined.push({index: p[0].index, char: p[0].char, group: chargroups.length});
                examined.push({index: p[1].index, char: p[1].char, group: chargroups.length});
                chargroups.push(p);
            } else if (first &amp;&amp; second &amp;&amp; first.group != second.group){
                chargroups[first.group].push(...chargroups[second.group]);
                examined = examined.map(e =&gt; {
                    if (e.group == second.group) e.group = first.group;
                    return e;
                });
                chargroups.splice(second.group, 1);
            }
        });
        possibilities.forEach((p, i) =&gt; {
            if (p.size == 1) {
                let char = Array.from(p)[0];
                chargroups.forEach(c =&gt; {
                    if (c.find(ch =&gt; ch.index == i &amp;&amp; ch.char != char)) {
                        people.forEach((person, j) =&gt; {
                            if (data.knaves.find(j)) return;
                            // We don't need to check both characters; if one's in the group they both are.
                            if (c.find(ch =&gt; ch.index == person[0].index &amp;&amp; ch.char == person[0].char)) {
                                data.knaves.push(j);
                                detected = true;
                            }
                        });
                    }
                });
            } else if (p.size == 0) {
                // Uh oh... need to reset.
                data.knaves = [];
                data.attestations.forEach((a, i) =&gt; {
                    if (a &lt;= data.threshold) data.knaves.push(i);
                });
                detected = true;
                // Don't get stuck in a loop.
                if (loop) data.threshold--;
                loop = true;
            }
        });
    }
    data.guess = possibilities.map(p =&gt; Array.from(p)[0]).join(&quot;&quot;);
    return data.guess;
}

This bot lets out exactly zero information.  Whenever it's asked about something, it responds by first asking what that bot thinks, and then it replies with that data.
It counts up the number of times that other bots have given it one of the values of one of the people, and uses that to give a rough confidence score to each person, tentatively marking someone as a knave if they hit -2.  If it hits a paradox, it resets who it's considering as a knave, but only automatically re-marks someone as a knave if they have a score of -5 or worse.
Because this bot doesn't aggressively attempt to solve, it's quite possible it'll get stuck on one or two people for a long time; I'm interested in how well it will do.  I haven't tested it yet, as it seems the controller is still (slightly) in flux.