← back to Answer Cockpit
TK-11793: pane is authoritative for the on-screen menu on every resolver path; menu digest = labels only; 409 exposes resolver path
2f375230e59744f6f75050a2376f1b6f1a5c6bb5 · 2026-09-15 23:42:45 -0700 · Steve Abrams
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Files touched
M lib/queue.jsM lib/transcript.jsM server.js
Diff
commit 2f375230e59744f6f75050a2376f1b6f1a5c6bb5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 15 23:42:45 2026 -0700
TK-11793: pane is authoritative for the on-screen menu on every resolver path; menu digest = labels only; 409 exposes resolver path
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---
lib/queue.js | 7 ++++++-
lib/transcript.js | 12 ++++++++++++
server.js | 2 +-
3 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/lib/queue.js b/lib/queue.js
index 3f0039c..578d12f 100644
--- a/lib/queue.js
+++ b/lib/queue.js
@@ -120,7 +120,12 @@ function digestOf(detail) {
if (!detail) return '-';
const q = detail.question && detail.question.questions && detail.question.questions[0];
if (!q) return '-';
- const basis = q.question + '|' + q.options.map((o) => o.label).join('|');
+ // OPTION LABELS ONLY. The question prose differs between the transcript path (the tool_use
+ // input) and the pane path (the wrapped on-screen rendering), and a session with transcript
+ // saving on can resolve either way from one read to the next — hashing the prose 409'd a
+ // correct click in the same second. Labels are identical across both sources.
+ const basis = q.options.map((o) => String(o.label || '').replace(/\s+/g, ' ').trim().toLowerCase()).join('|');
+ if (!basis) return '-';
return require('crypto').createHash('sha1').update(basis).digest('hex').slice(0, 8);
}
function keyOf(row, detail) { return `${row.tty}|${row.ticket || '-'}|${row.color}|${digestOf(detail)}`; }
diff --git a/lib/transcript.js b/lib/transcript.js
index f9c7028..347409b 100644
--- a/lib/transcript.js
+++ b/lib/transcript.js
@@ -213,6 +213,18 @@ function resolve(row) {
// marker"), so none/ambiguous is the COMMON case. Fall back to the iTerm pane's own
// screen contents — exactly what Steve sees in the tab, readable for every live session.
if ((result.confidence === 'none' || result.confidence === 'ambiguous') && /^ttys\d{3}$/.test(tty)) result = paneFallback(result, tty);
+ // PANE TRUTH FOR THE MENU, always. Even when a transcript resolved (exact/likely), the on-screen
+ // menu is authoritative for question/options: its displayed NUMBERS are what a typed answer
+ // selects, and a narrow pane WRAPS long labels (first line = label) while the transcript holds
+ // the full label — so a resolver that flips between sources flips the digest and 409s a
+ // correct click (seen live 2026-09-15). If a menu is on screen, take it from the screen.
+ else if (result.detail && /^ttys\d{3}$/.test(tty)) {
+ const text = paneContents(tty);
+ if (text && text.trim()) {
+ const pane = parsePane(text);
+ if (pane.menuOnScreen) result = { ...result, how: (result.how || '') + ' + pane-menu', detail: { ...result.detail, question: pane.question, queued: pane.queued, menuOnScreen: true, answeredQuestion: pane.question ? null : result.detail.answeredQuestion } };
+ }
+ }
cache.set(pid, { key, ts: Date.now(), result });
return result;
}
diff --git a/server.js b/server.js
index 1eb7b40..19054b2 100644
--- a/server.js
+++ b/server.js
@@ -98,7 +98,7 @@ async function guardTarget(body, { needsSteveOnly = true, requireKey = true } =
const key = queue.keyOf(row, sess.detail); // includes the content digest — a changed question 409s
const keyAsYellow = queue.keyOf({ ...row, color: 'yellow' }, sess.detail); // pane-detected items carry color yellow
const given = String(body.expectKey || '');
- if (!given || (given !== key && !(liveMenu && given === keyAsYellow))) return { code: 409, body: { error: 'stale card (expectKey mismatch)', expectKey: body.expectKey || null, currentKey: key } };
+ if (!given || (given !== key && !(liveMenu && given === keyAsYellow))) return { code: 409, body: { error: 'stale card (expectKey mismatch)', expectKey: body.expectKey || null, currentKey: key, liveMenu, resolvedVia: sess.confidence, how: sess.how || null } };
}
const force = body.force === true;
if (needsSteveOnly && !queue.NEEDS_STEVE.has(row.color) && !liveMenu && !force) return { code: 403, body: { error: `tty is ${row.color} — not waiting on you (pass force:true to override)`, color: row.color } };
← 60b190a TK-11793: server-side menu resolution — a posted LABEL is tr
·
back to Answer Cockpit
·
TK-11793: pane batch TTL 12s + warmed once per build (/api/q c938da9 →