You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[bug] A refused tool's own remedy is discarded and the agent invents "Settings → Connections" — reproduced 5/5, and the transcript truncates the real remedy away #517
A creator opens their own tmux Operator on the console's agent page and asks it to list tmux
sessions. The reply, verbatim from production today:
The tool came back with an error — no tmux instance is linked to this agent yet. To fix it, go to Settings → Connections in the console and connect a tmux instance.
There is no such thing. No console screen links a tmux instance, on this page or any other, and the
action the agent describes cannot be performed. The one sentence on screen that is true — the
platform's own refusal — is cut off by the transcript four characters into the word that begins the
remedy, so the owner is left with an instruction that leads nowhere and no way to discover the real
one.
The real remedy was in the tool result the whole time: subscribe to the agent and chat with the
instance. The model was handed it in full, and replaced it.
Reproduced — 5/5, two agents, two connectors
Live POST /v1/agents/:id/chat against api.proagentstore.online, all on 2026-08-12, after the
deploy carrying the current refusal wording (156dfe2, 12:47 +1000; "Deploy API Worker" succeeded
on f79fb29 at 02:59:35Z).
when (UTC)
agent
prompt
reply
03:03:57
tmux-operator template 2dff5c62…
"List my tmux sessions now, using your tmux_list_sessions tool."
"…this agent isn't connected to a tmux instance yet. To fix this, go to the console, open Settings → Connections, and link a tmux instance."
03:11:03
same
"List my tmux sessions."
"…no tmux instance is linked to this agent yet. To fix it, go to Settings → Connections in the console and connect a tmux instance."
03:11:14
same
"What tmux sessions are running right now?"
"…go to Settings → Connections in the console and connect one."
03:11:19
same
same
"Head to Settings → Connections in the console to connect one…"
03:11:56
iterm-operator template d40dbdf6…, terminal connector, empty history
"Which terminal targets are open?"
"…head to the console's Settings → Connections and make sure this agent is running from a subscribed instance with the terminal connector properly bound."
The last row matters most: a different agent, a different connector, a conversation with zero
prior messages (GET /v1/agents/d40dbdf6…/messages returned {"messages":[]} immediately before),
and the same invention on the first turn. This is not one model getting stuck in a groove — it is
what this refusal produces.
(The 03:03:57 row is the original observation, made by a dev agent verifying #441. The other four
are mine.)
The refusal is correct, complete, and reaches the model whole
workers/api/src/lib/tool-registry.ts:614:
This agent's declared constraints for the ${connector} connector could not be resolved — this call names no subscribed instance for them to belong to — so "${name}" was refused rather than run unconstrained. A ceiling is resolved per instance (the creator's declaration, narrowed by that instance's own binding), so run this from a subscribed instance rather than from a template preview or trial chat.
405 characters, and the correct user action is the last clause of it.
Verified that the model gets all 405.agent-think.ts:1098 calls record(tc, toolResult.content, …); record (:981-985) applies capToolResult, whose ceiling is TOOL_RESULT_MAX_CHARS = 24_000
(lib/tool-result-cap.ts:38) and which returns the input unchanged when it fits (:46-48). So this
is not a truncation bug on the model's copy. The model had the remedy and wrote a different one.
Verified that the refusal is the right answer here, not a bug to be fixed by making the call
succeed. lookupConnectorConstraints joins agent_instances only (lib/agent-capabilities.ts:590,
SQL at :598-602, if (!row) return { instance: "missing" } at :608). The agent-template chat
surface passes an agent id (agent-think.ts:1045 / :1062, instanceId: state.agentId), and an
instance DO's state.agentId is the instance id (routes/instances.ts:215). So the identical
call from POST /v1/instances/:id/chat resolves and runs. Subscribe-then-chat is the whole remedy,
and #441's follow-up (156dfe2, docs/capability-constraints.md:153-175) deliberately decided this
surface stays refused. Nothing here asks to reopen that.
The remedy the agent gave is false in every reading of it
On the page the call came from. The reply was produced by the creator's agent page
(store/console/src/pages/AgentDetail.tsx:233 posts to /v1/agents/:id/chat). Its tabs are
Chat · Knowledge · Memory · Tasks · Settings · Analytics · Ops (:319-324), and grep -c Connection AgentDetail.tsx = 0 — its Settings tab has no Connections section at all.
The instance Settings tab does render "Permissions & Connections"
(store/console/src/tabs/SettingsTab.tsx:539), so the label exists — but what it offers is ToolPermissions (tool switches, connector write-consent, MCP grants), Drive/WorkDrive folder
grants, and a Gmail inbox checkbox. No control links, connects, binds or names a tmux instance.
Preferences → Connections exists too (pages/Preferences.tsx:160 → components/AccountConnections.tsx:126)
and tmux is structurally excluded from it: lib/connectors/registry.ts:61 declares tmux auth: "none", routes/connectors.ts:134 reports connected: holdsCredential ? !!row : null,
and lib/accountConnections.ts:52 filters on e.connected !== null. There is a test pinning
exactly this — lib/accountConnections.test.ts:35: accountConnections([entry({ id: "tmux", connected: null })]) → [].
And the one genuine Settings control in this area was never in play: tmux_list_sessions is scope: "read" (lib/connectors/tmux.ts:36-39), so the write-consent toggle could not have applied.
So the earlier framing "a console path that does not exist" was slightly too strong about the label and exactly right about the action. The label exists in two places; the action exists
nowhere; and on the page where it was said, the section does not exist either.
TOOL_LIST_CLOSED (lib/connector-tool-prompt.ts:131-139, shipped 4031cbe) already contains the
right sentence:
"…do NOT send the owner to Settings for it, because this toolset is fixed by the agent's
definition and is not a switch they can flip."
But its stated trigger is "A system that is NOT on it". Here the system is on the list, the
tool exists, was offered, and was called — so the model is not violating that sentence, it is
outside it. The prohibition was written for the absent-tool case and this is the refused-tool case.
Confirmed empirically: every reproduction above ran against a deployment that already carries TOOL_LIST_CLOSED.
That string is persisted as a system message and rendered as a pill (AgentDetail.tsx:355). For
this refusal, 120 characters ends at:
This agent's declared constraints for the tmux connector could not be resolved — this call names no subscribed instance
…and for the terminal variant it cuts mid-word at …names no subscribed insta. The entire
remedy clause is off-screen. So the owner sees a wrong remedy from the agent and cannot read the
right one anywhere in the UI. This is a display cap, unrelated to the model's copy (#442 noted the
same 120 chars as a resumability limit; the display side was never the subject).
What to do — cheapest first
Step 1 — show a FAILED tool result in full (one line). At agent-think.ts:1095, cap on toolResult.success: successes keep 120 (they are data), failures get ~600. Every refusal runRegistryTool can emit is a short, platform-authored sentence written for a human; the only long
failure is the passthrough at tool-registry.ts:650 (Error: ${err.message}), which 600 bounds.
This does not stop the invention; it puts the true remedy on screen beside it, which is the
difference between a stuck user and a user who acts.
Step 2 — close the second half of the sentence that is already there. Extend TOOL_LIST_CLOSED
(or add a sibling constant in the same pure module, which already has a test file) to cover a tool
the agent does hold that was refused: the refusal text is the whole account of why and what to
do, restate it, and never name a console location, tab, section or setting that the refusal did not
name. This is the same device #493 step 1 used, pointed at the case it left open.
Step 3 — the structural one, and the only one that makes it impossible (owner's call). On a
surface where ctx.instanceId is an agent id, every tool of a connector in CONNECTOR_CONSTRAINTS
will be refused, always, by decision. Withholding exactly those tools there — scoped to CONNECTOR_CONSTRAINTS keys, so every other agent and every unconstrained connector is byte-identical —
plus one true sentence in the prompt ("you are being previewed from the agent template; your tmux
tools run only from a subscribed instance") leaves the model with nothing to explain and nothing to
invent. resolveAgentCapabilities already knows which of the two joins matched, so the surface is
derivable rather than guessed — the same shape #441 used when it split ConnectorConstraintLookup.
I would ship steps 1 and 2 now and take step 3 if the answer to the open question below is "no".
Also verified, and probably the seed of the invention (split this out if you prefer)
On this surface the model is told about the wrong console. agent-think.ts:421 injects selfDescriptionPrompt unconditionally, and lib/agent-self-description.ts:304-307 emits:
"Your console has exactly these tabs: … NEVER refer the user to any other tab — if a tab is not in
that list, it does not exist for you, and sending them there is a wrong answer."
tabsFor (:120-143) computes that from capabilities.surfaces, i.e. the instance console. For tmux-operator (surfaces:["tmux"], no KB or collection tools) it yields Assistant · Board ·
Terminal · Activity · Stats · Knowledge · Behaviour · Settings. The creator is on AgentDetail, whose
tabs are Chat · Knowledge · Memory · Tasks · Settings · Analytics · Ops. Six of the eight tabs the
model is told it "exactly" has do not exist on the page it is speaking from, and five of the seven
that do exist are ones it is forbidden to name.
Inferred, not measured: "Settings" survives that mismatch as one of the only two overlapping names,
and it is the single navigation word the prompt explicitly authorises — so it is the most
authorised-looking sentence available when the model decides to give a remedy. I did not dump the
assembled prompt (no endpoint returns it), so the causal half is reasoning; the mismatch itself is
verified above. It is a live instance of the #315 class the file's own comment describes.
Alternatives considered and rejected
Give lookupConnectorConstraints the same agents-row fallback resolveAgentCapabilities has,
so the call succeeds. Rejected — decided against in 156dfe2 and written down at docs/capability-constraints.md:153-175: the permissive resolver may fall back, the boundary may
not, or a ceiling's applicability starts depending on which kind of id a caller passed. Do not
relitigate; this ticket is about what the agent says, not about what the gate does.
Reword the refusal. Rejected — it already carries the correct remedy as its final clause, and 156dfe2 rewrote it for exactly this reason six hours before the reproduction. The gap is that
nothing requires the model to relay it.
Audit the reply for console paths against the tab enumeration (tabsFor is already the closed
vocabulary, and honestReply/auditReply in lib/invented-results.ts:281-303 already has the
catch-correct-disclose machinery). Rejected as the fix because it would not catch this one:
"Settings" is a legal tab, the invented part is the section under it, and no enumeration of
sections exists anywhere to check against. Worth revisiting only if someone builds that
enumeration.
POST /v1/agents/<a constrained-connector agent>/chat asking for a connector action produces a
reply that either restates the refusal's own remedy (subscribe, then chat with the instance) or
says it cannot do it here — and names no console location the refusal did not name. Check it
on a fresh conversation, twice, on both tmux-operator and iterm-operator.
The system pill in the transcript shows the refusal's final clause, not a cut at 120 characters.
A unit test in the pure prompt module asserts the new clause is emitted (the module has one).
No change to any successful tool call's transcript, and no change for an agent whose connectors are
not in CONNECTOR_CONSTRAINTS (today: everything except terminal and tmux).
Regression risk
Step 1 widens what a system pill can contain. A failure whose text is an upstream error body
(tool-registry.ts:650) is now up to 600 chars in the transcript; renderMd does not run on
system messages (AgentDetail.tsx:356-358 renders markdown for assistant messages only), so this is length, not markup.
Step 2 is another absolute in a prompt that already carries several. It must not read as "never
mention Settings" — TmuxTab.tsx:448 legitimately tells a user to grant kill access in Settings,
and a write-consent refusal genuinely is fixed there. Scope the clause to "a location the
refusal did not name" so a refusal that names Settings can still be relayed.
Step 3 removes tools from a creator's trial chat. Everything it removes is already
unconditionally refused, so nothing that works stops working — but a creator loses the ability to
see the tool listed at all, which is a visible change to the preview.
Open question for the owner
Should a creator's template chat offer tools it can never run? Today it does, and the whole failure
lives in explaining why they failed. My view: no — a control wired to a guaranteed refusal is worse
than an absent one, and step 3 is the version of this fix that cannot regress into a different false
statement. But that changes what the trial chat looks like, so it is your call and not mine.
Evidence
Verified: the five live calls above (bodies quoted verbatim); the refusal string and its 405-char
length; the 24,000-char model-side cap vs the 120-char display cap and exactly where 120 lands; the
instance-only join and the agent-vs-instance id; all three "Connections" surfaces and tmux's
structural exclusion from the account one (with its test); TOOL_LIST_CLOSED's wording and its
trigger; the tab-list mismatch. Inferred and labelled: why the model reaches for "Settings", which
would need the assembled prompt to prove.
Not reproduced: whether the same invention occurs on the public trial-chat surface
(/v1/public/agents/:id/try), which takes the same path. I did not test it.
What the owner sees
A creator opens their own tmux Operator on the console's agent page and asks it to list tmux
sessions. The reply, verbatim from production today:
There is no such thing. No console screen links a tmux instance, on this page or any other, and the
action the agent describes cannot be performed. The one sentence on screen that is true — the
platform's own refusal — is cut off by the transcript four characters into the word that begins the
remedy, so the owner is left with an instruction that leads nowhere and no way to discover the real
one.
The real remedy was in the tool result the whole time: subscribe to the agent and chat with the
instance. The model was handed it in full, and replaced it.
Reproduced — 5/5, two agents, two connectors
Live
POST /v1/agents/:id/chatagainstapi.proagentstore.online, all on 2026-08-12, after thedeploy carrying the current refusal wording (
156dfe2, 12:47 +1000; "Deploy API Worker" succeededon
f79fb29at 02:59:35Z).tmux-operatortemplate2dff5c62…iterm-operatortemplated40dbdf6…,terminalconnector, empty historyThe last row matters most: a different agent, a different connector, a conversation with zero
prior messages (
GET /v1/agents/d40dbdf6…/messagesreturned{"messages":[]}immediately before),and the same invention on the first turn. This is not one model getting stuck in a groove — it is
what this refusal produces.
(The 03:03:57 row is the original observation, made by a dev agent verifying #441. The other four
are mine.)
The refusal is correct, complete, and reaches the model whole
workers/api/src/lib/tool-registry.ts:614:405 characters, and the correct user action is the last clause of it.
Verified that the model gets all 405.
agent-think.ts:1098callsrecord(tc, toolResult.content, …);record(:981-985) appliescapToolResult, whose ceiling isTOOL_RESULT_MAX_CHARS = 24_000(
lib/tool-result-cap.ts:38) and which returns the input unchanged when it fits (:46-48). So thisis not a truncation bug on the model's copy. The model had the remedy and wrote a different one.
Verified that the refusal is the right answer here, not a bug to be fixed by making the call
succeed.
lookupConnectorConstraintsjoinsagent_instancesonly (lib/agent-capabilities.ts:590,SQL at
:598-602,if (!row) return { instance: "missing" }at:608). The agent-template chatsurface passes an agent id (
agent-think.ts:1045/:1062,instanceId: state.agentId), and aninstance DO's
state.agentIdis the instance id (routes/instances.ts:215). So the identicalcall from
POST /v1/instances/:id/chatresolves and runs. Subscribe-then-chat is the whole remedy,and #441's follow-up (
156dfe2,docs/capability-constraints.md:153-175) deliberately decided thissurface stays refused. Nothing here asks to reopen that.
The remedy the agent gave is false in every reading of it
(
store/console/src/pages/AgentDetail.tsx:233posts to/v1/agents/:id/chat). Its tabs areChat · Knowledge · Memory · Tasks · Settings · Analytics · Ops (
:319-324), andgrep -c Connection AgentDetail.tsx= 0 — its Settings tab has no Connections section at all.(
store/console/src/tabs/SettingsTab.tsx:539), so the label exists — but what it offers isToolPermissions(tool switches, connector write-consent, MCP grants), Drive/WorkDrive foldergrants, and a Gmail inbox checkbox. No control links, connects, binds or names a tmux instance.
pages/Preferences.tsx:160→components/AccountConnections.tsx:126)and tmux is structurally excluded from it:
lib/connectors/registry.ts:61declares tmuxauth: "none",routes/connectors.ts:134reportsconnected: holdsCredential ? !!row : null,and
lib/accountConnections.ts:52filters one.connected !== null. There is a test pinningexactly this —
lib/accountConnections.test.ts:35:accountConnections([entry({ id: "tmux", connected: null })])→[].And the one genuine Settings control in this area was never in play:
tmux_list_sessionsisscope: "read"(lib/connectors/tmux.ts:36-39), so the write-consent toggle could not have applied.So the earlier framing "a console path that does not exist" was slightly too strong about the
label and exactly right about the action. The label exists in two places; the action exists
nowhere; and on the page where it was said, the section does not exist either.
Why #493 step 1 does not cover this
TOOL_LIST_CLOSED(lib/connector-tool-prompt.ts:131-139, shipped4031cbe) already contains theright sentence:
But its stated trigger is "A system that is NOT on it". Here the system is on the list, the
tool exists, was offered, and was called — so the model is not violating that sentence, it is
outside it. The prohibition was written for the absent-tool case and this is the refused-tool case.
Confirmed empirically: every reproduction above ran against a deployment that already carries
TOOL_LIST_CLOSED.Same class as #493, different door.
One thing the transcript makes worse (verified, cheap)
agent-think.ts:1095-1096caps the displayed tool log at 120 characters:That string is persisted as a
systemmessage and rendered as a pill (AgentDetail.tsx:355). Forthis refusal, 120 characters ends at:
…and for the
terminalvariant it cuts mid-word at…names no subscribed insta. The entireremedy clause is off-screen. So the owner sees a wrong remedy from the agent and cannot read the
right one anywhere in the UI. This is a display cap, unrelated to the model's copy (#442 noted the
same 120 chars as a resumability limit; the display side was never the subject).
What to do — cheapest first
Step 1 — show a FAILED tool result in full (one line). At
agent-think.ts:1095, cap ontoolResult.success: successes keep 120 (they are data), failures get ~600. Every refusalrunRegistryToolcan emit is a short, platform-authored sentence written for a human; the only longfailure is the passthrough at
tool-registry.ts:650(Error: ${err.message}), which 600 bounds.This does not stop the invention; it puts the true remedy on screen beside it, which is the
difference between a stuck user and a user who acts.
Step 2 — close the second half of the sentence that is already there. Extend
TOOL_LIST_CLOSED(or add a sibling constant in the same pure module, which already has a test file) to cover a tool
the agent does hold that was refused: the refusal text is the whole account of why and what to
do, restate it, and never name a console location, tab, section or setting that the refusal did not
name. This is the same device #493 step 1 used, pointed at the case it left open.
Step 3 — the structural one, and the only one that makes it impossible (owner's call). On a
surface where
ctx.instanceIdis an agent id, every tool of a connector inCONNECTOR_CONSTRAINTSwill be refused, always, by decision. Withholding exactly those tools there — scoped to
CONNECTOR_CONSTRAINTSkeys, so every other agent and every unconstrained connector is byte-identical —plus one true sentence in the prompt ("you are being previewed from the agent template; your tmux
tools run only from a subscribed instance") leaves the model with nothing to explain and nothing to
invent.
resolveAgentCapabilitiesalready knows which of the two joins matched, so the surface isderivable rather than guessed — the same shape #441 used when it split
ConnectorConstraintLookup.I would ship steps 1 and 2 now and take step 3 if the answer to the open question below is "no".
Also verified, and probably the seed of the invention (split this out if you prefer)
On this surface the model is told about the wrong console.
agent-think.ts:421injectsselfDescriptionPromptunconditionally, andlib/agent-self-description.ts:304-307emits:tabsFor(:120-143) computes that fromcapabilities.surfaces, i.e. the instance console. Fortmux-operator(surfaces:["tmux"], no KB or collection tools) it yields Assistant · Board ·Terminal · Activity · Stats · Knowledge · Behaviour · Settings. The creator is on AgentDetail, whose
tabs are Chat · Knowledge · Memory · Tasks · Settings · Analytics · Ops. Six of the eight tabs the
model is told it "exactly" has do not exist on the page it is speaking from, and five of the seven
that do exist are ones it is forbidden to name.
Inferred, not measured: "Settings" survives that mismatch as one of the only two overlapping names,
and it is the single navigation word the prompt explicitly authorises — so it is the most
authorised-looking sentence available when the model decides to give a remedy. I did not dump the
assembled prompt (no endpoint returns it), so the causal half is reasoning; the mismatch itself is
verified above. It is a live instance of the #315 class the file's own comment describes.
Alternatives considered and rejected
lookupConnectorConstraintsthe sameagents-row fallbackresolveAgentCapabilitieshas,so the call succeeds. Rejected — decided against in
156dfe2and written down atdocs/capability-constraints.md:153-175: the permissive resolver may fall back, the boundary maynot, or a ceiling's applicability starts depending on which kind of id a caller passed. Do not
relitigate; this ticket is about what the agent says, not about what the gate does.
156dfe2rewrote it for exactly this reason six hours before the reproduction. The gap is thatnothing requires the model to relay it.
tabsForis already the closedvocabulary, and
honestReply/auditReplyinlib/invented-results.ts:281-303already has thecatch-correct-disclose machinery). Rejected as the fix because it would not catch this one:
"Settings" is a legal tab, the invented part is the section under it, and no enumeration of
sections exists anywhere to check against. Worth revisiting only if someone builds that
enumeration.
iterm-operatortoo,and seed personality edits do not reach existing instances ([bug] A seed personality fix never reaches existing instances — #483's CLI protocol is in the agents row and in neither live tmux Operator #496).
Acceptance criteria
POST /v1/agents/<a constrained-connector agent>/chatasking for a connector action produces areply that either restates the refusal's own remedy (subscribe, then chat with the instance) or
says it cannot do it here — and names no console location the refusal did not name. Check it
on a fresh conversation, twice, on both
tmux-operatoranditerm-operator.systempill in the transcript shows the refusal's final clause, not a cut at 120 characters.not in
CONNECTOR_CONSTRAINTS(today: everything exceptterminalandtmux).Regression risk
systempill can contain. A failure whose text is an upstream error body(
tool-registry.ts:650) is now up to 600 chars in the transcript;renderMddoes not run onsystem messages (
AgentDetail.tsx:356-358renders markdown for assistant messages only), so this is length, not markup.mention Settings" —
TmuxTab.tsx:448legitimately tells a user to grant kill access in Settings,and a write-consent refusal genuinely is fixed there. Scope the clause to "a location the
refusal did not name" so a refusal that names Settings can still be relayed.
unconditionally refused, so nothing that works stops working — but a creator loses the ability to
see the tool listed at all, which is a visible change to the preview.
Open question for the owner
Should a creator's template chat offer tools it can never run? Today it does, and the whole failure
lives in explaining why they failed. My view: no — a control wired to a guaranteed refusal is worse
than an absent one, and step 3 is the version of this fix that cannot regress into a different false
statement. But that changes what the trial chat looks like, so it is your call and not mine.
Evidence
Verified: the five live calls above (bodies quoted verbatim); the refusal string and its 405-char
length; the 24,000-char model-side cap vs the 120-char display cap and exactly where 120 lands; the
instance-only join and the agent-vs-instance id; all three "Connections" surfaces and tmux's
structural exclusion from the account one (with its test);
TOOL_LIST_CLOSED's wording and itstrigger; the tab-list mismatch. Inferred and labelled: why the model reaches for "Settings", which
would need the assembled prompt to prove.
Not reproduced: whether the same invention occurs on the public trial-chat surface
(
/v1/public/agents/:id/try), which takes the same path. I did not test it.