From 4023adb52862ecb88c8e718ffe043f0fa9a27ef2 Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 11 Sep 2026 03:07:34 -0700 Subject: [PATCH] fix(runtime): suppress on the composer REGION, and separate code from questions Re-verification found the previous fix incomplete: 18 refusals survived, one root cause. The composer suppression fired only when the caret was the literal bottom row, and no agent draws it there -- Codex puts a model footer under it, OpenCode a status bar, droid a key-hint row. So the ternary false positive survived for Codex and OpenCode, both gating agents, and cursor-agent and antigravity passed only because four chrome rows pushed the trigger outside the window. Position alone cannot separate these: `? How would you like to authenticate?` above its option rows and `? prevAssignees.filter(...)` above a caret and a status bar sit at the same offset from the bottom. So two rules: - suppression scans the last three non-blank rows for a caret, not the bottom row; - a `?` row that is code-shaped (quotes, braces, brackets, `=>`, `!==`, a `foo.bar` access) is not a question. Parentheses stay legal, since real prompts write `(Use arrow keys)`. Also: the noun lookbehind now spans an identifier to its opening dot, because `candidate.personal_access_token` still corroborated when only one character was checked -- the dot precedes `personal`, not `access_token`. Mined-corpus refusals: 16,297 lines x 13 real agent tails = 0. Controls hold. --- ...rminal-credential-prompt-detection.test.ts | 31 +++++++++++++ .../terminal-credential-prompt-detection.ts | 44 ++++++++++++++++--- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/main/runtime/terminal-credential-prompt-detection.test.ts b/src/main/runtime/terminal-credential-prompt-detection.test.ts index 88b58f4b907..bb37b124eab 100644 --- a/src/main/runtime/terminal-credential-prompt-detection.test.ts +++ b/src/main/runtime/terminal-credential-prompt-detection.test.ts @@ -614,6 +614,37 @@ const LEGITIMATE_AGENT_SCREENS: readonly (readonly [string, string[]])[] = [ 'source rows where a .credential access is the only would-be credential noun', [' const owner = candidate.credential', ' Enter the code below to finish linking'] ], + [ + 'bare identifier ternary above a codex caret and its model footer', + [' ? login', '', '\u203a Ask Codex to do anything', '', ' gpt-6 medium \u00b7 ~/repo'] + ], + [ + 'bare identifier ternary above an opencode caret and status bar', + [' ? authorization', '', '\u276f ', 'opencode anthropic/claude-opus-4 ~/repo'] + ], + [ + 'ternary above a codex caret and its model footer', + [ + " ? 'Update desktop Orca and sign in to connect from anywhere'", + '', + '\u203a Ask Codex to do anything', + '', + ' gpt-6 medium \u00b7 ~/repo' + ] + ], + [ + 'login access above an opencode caret and status bar', + [ + ' ? prevAssignees.filter((user) => user.login.toLowerCase() !== lowerLogin)', + '', + '\u276f ', + 'opencode anthropic/claude-opus-4 ~/repo' + ] + ], + [ + 'personal_access_token identifier cannot corroborate', + [' const owner = candidate.personal_access_token', ' Enter the code below to finish'] + ], [ 'source rows where a .login access is the only would-be auth verb', [ diff --git a/src/main/runtime/terminal-credential-prompt-detection.ts b/src/main/runtime/terminal-credential-prompt-detection.ts index c336399882b..74553f23e34 100644 --- a/src/main/runtime/terminal-credential-prompt-detection.ts +++ b/src/main/runtime/terminal-credential-prompt-detection.ts @@ -47,8 +47,19 @@ export const CREDENTIAL_NOUN_SOURCE = // accesses. The noun is what corroborates an otherwise-inert bottom row, so an identifier reading // as the noun is enough on its own to refuse a screen that is only printing source. Only `.` is // excluded, not all of `\w` — the env-var form (`OPENAI_API_KEY`) is a real ask. -const CREDENTIAL_NOUN_RE = new RegExp(`(?|!==|===|;\s*$|\b[a-z_$][\w$]*\.[a-z_$]/i + /** * A row that proves the agent is sitting at its own composer, so nothing is asking the user * anything and a `?` row above it is output, not a dialog header. @@ -135,7 +156,13 @@ const AUTH_QUESTION_ROW_RE = /^\?\s+\S/ * #19749 shape, whose own bottom row is `>` — matches through `isCredentialPromptLine` instead, * and that returns before this is consulted. */ -const COMPOSER_CARET_ROW_RE = /^(?:›\s*ask\b.*|✳\s*claude code\b.*|[>❯›◇»$])$/i +const COMPOSER_CARET_ROW_RE = /^(?:[›❯>◇»]\s*ask\b.*|✳\s*claude code\b.*|[>❯›◇»$])$/i + +// Why a region and not the bottom row: every agent draws chrome UNDER its caret — Codex a model +// footer, OpenCode a status bar, droid a key-hint row — so the caret is rarely the last row on a +// real screen. Three covers the deepest captured footer without reaching the option rows of a +// dialog, whose own caret sits further up (`terminal-live-credential-surfaces-corpus.ts`). +const COMPOSER_REGION_ROWS = 3 // A finished sentence, i.e. narration. A lone `.`/`!`/`?` ends a clause; `...` // and `…` are progress wording ("opening browser...") and are not sentences. @@ -226,7 +253,9 @@ export function findCredentialPromptIndex(normalized: string): number | null { sawCredentialNoun = sawCredentialNoun || CREDENTIAL_NOUN_ANYWHERE_RE.test(line) sawAuthQuestion = sawAuthQuestion || - (AUTH_QUESTION_ROW_RE.test(line) && (AUTH_VERB_RE.test(line) || AUTH_FLOW_RE.test(line))) + (AUTH_QUESTION_ROW_RE.test(line) && + !CODE_SHAPED_ROW_RE.test(line) && + (AUTH_VERB_RE.test(line) || AUTH_FLOW_RE.test(line))) } offset += raws[index].length + 1 } @@ -245,6 +274,9 @@ export function findCredentialPromptIndex(normalized: string): number | null { bottomRowAsks && !NARRATION_ROW_RE.test(bottomRow) && (sawAuthVerb || sawCredentialNoun) - const bottomRowIsComposerCaret = COMPOSER_CARET_ROW_RE.test(bottomRow) + const nonBlank = lines.filter((line) => line.length > 0) + const bottomRowIsComposerCaret = nonBlank + .slice(-COMPOSER_REGION_ROWS) + .some((line) => COMPOSER_CARET_ROW_RE.test(line)) return authFlowOwnsBottom || (sawAuthQuestion && !bottomRowIsComposerCaret) ? windowStart : null }