We run a long-lived coding agent on a computer, and people can reach it in two ways: through the app, and through a chat bot door that passes their messages to it. One evening the chat bot door answered a request with “the agent is closed on this computer, open it in the app”. The agent was open in the app. The user sent the request again and got the same reply, three times in a row. Nothing had crashed. The door was asking the wrong question of a lookup that could not tell “none” from “more than one”.

Key facts

  • The chat bot door said the agent was closed three times in a row while the agent was open in the app.
  • The door trusted its lookup only when exactly one terminal session looked like the agent.
  • A background worker session running in the agent's own folder also looked like the agent, so there were two matches.
  • Two matches returned nothing, and the door read that nothing as “not running”.
  • Fix: workers no longer count, the lookup ranks what is left and picks one, it logs what it saw when it finds nothing, and the door starts the agent when there really is none.

What happened?

The user was on a phone and asked the chat bot door for a piece of work. The door's job is simple: find the agent's terminal session on the computer, hand it the message, and relay the answer. If there is no agent session, it says so and tells the user how to start one.

The reply said the agent was closed. The user opened the app, saw the agent there and running, and asked again. Same reply. A third time, same reply. There was no error in any log, because from the code's point of view nothing had gone wrong. It had looked for the agent, decided there was none, and said so politely.

The one-line lookup

The door listed every terminal session on the computer and marked the ones that looked like the agent. A session counted if it sat in the workspace labelled for the agent, or if its working folder was the agent's folder. Then it did this:

const hit = want && rows.find((r) => r.pane === want);
if (hit) return hit;
const chiefs = rows.filter((r) => r.chief);
return chiefs.length === 1 ? chiefs[0] : null;

The first two lines honour a pinned session id when one is set. The last line is the one that matters. It reads like caution: only answer when the answer is certain. On that evening a background worker session was doing a job in the agent's folder. Worker sessions have no terminal and are not the agent, but the folder test could not know that. The list had two matches, and the line returned null.

Why “exactly one” fails closed both ways

An “exactly one or nothing” rule has two ways to return nothing. Zero matches gives null, which is honest: the agent is not there. Two or more matches also gives null, and the caller cannot tell the two apart. So “running twice” reaches the user as “not running”.

The second way hides, for three reasons. It looks exactly like the first, so anyone reading the reply or the code path believes it. It only shows up when something else starts sharing the folder, which here meant a background worker that came and went, so the bug seemed to come and go too. And the message names the wrong cure. “Open it in the app” sends the user to start another session, which in the worst case makes a third match.

We had met the same shape a few days earlier, from the other side, in a mirrored pipeline that silently lost orders. There a lookup with two matching records picked the wrong one without saying so. Here a lookup with two matches picked none without saying so. Both times the fix was to make the choice on purpose and leave a trace.

The fix

Before and after, cut down to the lines that carry the point:

// before
return chiefs.length === 1
  ? chiefs[0] : null;

// after
rank(s) =
  isWorker(s)          ? 0 :
  labelledWorkspace(s) ? 2 :
  sameRealpath(s.cwd, agentDir) ? 1 : 0
pick = rows.filter(rank > 0)
  .sort(byRank, idleFirst, byId)[0]
if (!pick) log("no agent among", rows)

Worker sessions are never candidates, wherever they run. What is left gets a rank: the labelled workspace beats a session that only shares the folder. Ties are broken the same way every time: an idle session before a busy one, then by session id. Folders are compared after resolving links, so a symlinked path no longer counts as a different folder. When the lookup does come up empty, it writes one log line naming the first few sessions it saw, with their workspace and folder, so the next “closed” can be explained in a minute. And when there really is no agent, the door now starts one itself and tells the user it is starting, instead of telling them to go and do it.

Three rules

  • Prefer a deterministic pick over “exactly one”. If several matches are possible, write down how to rank them and pick the best. A rule that gives up on two turns every new duplicate into an outage.
  • Log the candidates when a lookup comes up empty. One line with what was there and why none qualified. Without it, “none” and “too many” look the same in the logs too.
  • A “not running” message must come from a check that cannot be fooled by “running twice”. Only say something is absent when the check counted zero. Otherwise say what you actually saw.

Common questions

Isn't refusing to guess the safer choice?

Refusing is fine when the caller hears “ambiguous”. It is not fine when the caller hears “none”. If you will not pick, return the count or the list, so the message can say what is really going on.

Why not throw an error on two matches?

A throw that some caller catches and turns into “not running” is the same bug one layer up. Rank the candidates, pick one, and log the tie so a person can see it.

Sources