The ambient reply gate discards direct answers to questions qm itself asked, and fails closed
qm was asked to chase a colleague for an answer and report back. The colleague
answered, in the thread, under qm's own question. qm reacted with 👍 and never
reported. The requester was told an hour later that no answer had arrived.
This is not the model that serves the scope making a judgement call — it is a
separate gate, on a separate model, that runs only on messages qm was not
explicitly @-mentioned in, and that defaults to staying quiet whenever it is
uncertain or broken.
(Names below are from a test scenario on my own deployment, translated. The
timestamps, ids and flags are verbatim from Postgres.)
What happened
A private channel. founder asks qm to chase admin and report back within the
hour. Times are Asia/Taipei.
| time |
who |
message |
mentions_self |
handled |
| 22:13 |
founder |
"@qm John starts tomorrow, ask @Admin whether his insurance enrolment is done … report back to me in an hour" |
t |
t |
| 22:13 |
qm |
"@Admin John starts tomorrow, has his enrolment gone through? … @founder I'll report back in an hour" |
— |
— |
| 23:15 |
qm |
"@founder an hour's up: no reply from @Admin yet" |
— |
— |
| 23:26 |
admin |
"Sorry, didn't see the notification. Filed Friday, effective 8/17, all good." |
f |
t |
| 23:50 |
admin |
"<@B0BND0YAX4J> did admin answer about the insurance?" |
f |
f |
| 00:13 |
admin |
"Didn't I reply? Why are you saying I didn't?" |
f |
t |
The 23:26 row is the one that matters. handled = t — qm ingested it and ran a
turn. That turn's stored result:
{"status":"react","sessionId":"a11e5cb2-…","reactions":["+1"]}
It read the answer to its own question, gave it a thumbs-up, and dropped the
commitment it had made 73 minutes earlier.
Why the gate saw it at all
src/core/orchestrator.ts:398 marks a turn ambient when it arrives without an
explicit mention, and :1540 routes ambient turns through an extra model call
before any real work happens:
if (ambientTurn && deps.harness.models.shouldRespond && …) {
const decision = await deps.harness.models.shouldRespond({ … });
…
if (!decision.respond) { // :1565
return reactions ? { status: "react", … } : { status: "silent", … };
}
A message that @-mentions qm never reaches this branch. So the reliability of a
tracked hand-off depends on whether the person answering happens to type a
mention — which is precisely the thing qm told them they didn't need to do
("just reply in this thread").
The prompt already specifies the right answer
TURN_DETECTION_PROMPT_HEAD (src/harness/pi-harness.ts:131) covers this case
twice, explicitly:
:140 "it answers a question you just asked, says yes/no/go ahead/that/this in response
to your prior message" → YES
:159 "is posted in a thread the assistant itself STARTED … Someone replying under your
own message is almost always talking to you, even without naming you"→ YES
and ships a near-identical worked example:
"yes, send it" after you offered to send something → YES (a direct answer to you, no mention needed)
Admin's message was a direct answer to a question qm posted in that thread. Two
rules and one example all point at YES. The classifier returned REACT.
So this is not a gap in the prompt to be patched with another bullet. A
deterministic condition — qm asked a question in this thread and has not
recorded an answer — is being delegated to a probabilistic call that can, and
here did, get it wrong.
Two ways the gate fails closed
Uncertainty resolves to silence. pi-harness.ts:188: "When genuinely
unsure, prefer NO — a good colleague would rather stay quiet than barge in."
Sound default for chit-chat in a busy channel. Wrong default when qm is holding
an unfulfilled promise in the same thread, because there the cost of staying
quiet is not a missed pleasantry, it is a broken commitment that nobody is told
about.
A failed detection call also resolves to silence. claude-harness.ts:895:
} catch (error) {
swallow("claude: detect", error);
return { respond: false };
}
Any provider hiccup on the detect call — the same class of failure as #602 — silently converts into "don't answer."
Nothing is posted, nothing is surfaced to an operator, and the turn records a
silent status.
Worth noting the gate also runs on judgeModelId, not the model the scope is
pinned to. Whatever model an admin evaluated and selected for a channel is not
the model deciding whether that channel's messages deserve an answer.
Suggested fix
Skip the ambient gate — or force respond: true — when qm has an outstanding
commitment in the thread: it asked a question, promised a report, or set a
deadline that has not been discharged. That state is already known; the thread
session here is literally titled "Chase … status, report back", and the
scheduled report at 23:15 fired from it.
Failing that, at minimum:
catch → { respond: false } should not be indistinguishable from a
considered "no". Treat a detection failure as "respond" (the safe direction
when someone is waiting on you), or surface it.
- The "prefer NO when unsure" tie-break should invert in threads qm started or
asked a question in — the prompt already has the rule, it just loses to the
tie-break.
A related consequence is filed as #608: once the update was not pushed, the
requester could not pull it either.
I have the full transcript, run rows and session ids and can supply them, or
test a branch against a live deployment.
The ambient reply gate discards direct answers to questions qm itself asked, and fails closed
qm was asked to chase a colleague for an answer and report back. The colleague
answered, in the thread, under qm's own question. qm reacted with 👍 and never
reported. The requester was told an hour later that no answer had arrived.
This is not the model that serves the scope making a judgement call — it is a
separate gate, on a separate model, that runs only on messages qm was not
explicitly @-mentioned in, and that defaults to staying quiet whenever it is
uncertain or broken.
(Names below are from a test scenario on my own deployment, translated. The
timestamps, ids and flags are verbatim from Postgres.)
What happened
A private channel.
founderasks qm to chaseadminand report back within thehour. Times are Asia/Taipei.
mentions_selfhandledttftffftThe 23:26 row is the one that matters.
handled = t— qm ingested it and ran aturn. That turn's stored result:
{"status":"react","sessionId":"a11e5cb2-…","reactions":["+1"]}It read the answer to its own question, gave it a thumbs-up, and dropped the
commitment it had made 73 minutes earlier.
Why the gate saw it at all
src/core/orchestrator.ts:398marks a turn ambient when it arrives without anexplicit mention, and
:1540routes ambient turns through an extra model callbefore any real work happens:
A message that @-mentions qm never reaches this branch. So the reliability of a
tracked hand-off depends on whether the person answering happens to type a
mention — which is precisely the thing qm told them they didn't need to do
("just reply in this thread").
The prompt already specifies the right answer
TURN_DETECTION_PROMPT_HEAD(src/harness/pi-harness.ts:131) covers this casetwice, explicitly:
and ships a near-identical worked example:
Admin's message was a direct answer to a question qm posted in that thread. Two
rules and one example all point at YES. The classifier returned REACT.
So this is not a gap in the prompt to be patched with another bullet. A
deterministic condition — qm asked a question in this thread and has not
recorded an answer — is being delegated to a probabilistic call that can, and
here did, get it wrong.
Two ways the gate fails closed
Uncertainty resolves to silence.
pi-harness.ts:188: "When genuinelyunsure, prefer NO — a good colleague would rather stay quiet than barge in."
Sound default for chit-chat in a busy channel. Wrong default when qm is holding
an unfulfilled promise in the same thread, because there the cost of staying
quiet is not a missed pleasantry, it is a broken commitment that nobody is told
about.
A failed detection call also resolves to silence.
claude-harness.ts:895:Any provider hiccup on the detect call — the same class of failure as #602 — silently converts into "don't answer."
Nothing is posted, nothing is surfaced to an operator, and the turn records a
silentstatus.Worth noting the gate also runs on
judgeModelId, not the model the scope ispinned to. Whatever model an admin evaluated and selected for a channel is not
the model deciding whether that channel's messages deserve an answer.
Suggested fix
Skip the ambient gate — or force
respond: true— when qm has an outstandingcommitment in the thread: it asked a question, promised a report, or set a
deadline that has not been discharged. That state is already known; the thread
session here is literally titled "Chase … status, report back", and the
scheduled report at 23:15 fired from it.
Failing that, at minimum:
catch → { respond: false }should not be indistinguishable from aconsidered "no". Treat a detection failure as "respond" (the safe direction
when someone is waiting on you), or surface it.
asked a question in — the prompt already has the rule, it just loses to the
tie-break.
A related consequence is filed as #608: once the update was not pushed, the
requester could not pull it either.
I have the full transcript, run rows and session ids and can supply them, or
test a branch against a live deployment.