mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 08:11:17 -10:00
fix(session-transport): tighten idle-prompt discriminator per r2 adversarial review
Cycle-2 revision of 85d0844. r2 found the idle-prompt override
regex /^[❯›]\s/ was too broad — it matched any line starting with
the prompt char, including active input like '❯ Working on a task.'
where "Working" should trigger the mid-work guard.
Tightened to two complementary discriminators:
- IDLE_PROMPT_PATTERNS: /^[❯›]\s*$/ (empty prompt only — prompt
char + optional whitespace + end-of-line)
- IDLE_STATUS_BAR_PATTERNS: Codex model footer (gpt-N... Context)
and Claude edit-accept bar — these only render at idle prompt,
never during active tool execution
r2 fixture A (active Claude input '❯ Working on a task.') now
correctly blocks. r2 fixture B (trust-prompt '› 1. Yes, continue')
is documented as a pre-existing gap: no MID_WORK_PATTERNS match
that content, so looksLikeMidWork was never triggered pre- or
post-fix. Trust-prompt detection would require scope expansion
per PM constraint #4; filed as separate concern. Original
stale-scrollback fixes preserved (both Codex and Claude idle
tests still pass via the status-bar discriminator).
Tests: 25/25 session-transport (5 new: 2 stale-scrollback override,
1 genuine mid-work preserved, 1 r2 fixture A active-input blocks,
1 r2 fixture B trust-prompt documented). Full daemon: 1784/1784.
tsc clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
1 parent
85d0844303
commit
69479d9a39
2 files changed
+64
-7
No files matched your search
@@ -10,12 +10,19 @@ const MID_WORK_PATTERNS = [
|
||||
/esc to interrupt/,
|
||||
];
|
||||
|
||||
// Prompt indicators for Claude Code (❯) and Codex (›). When one of these
|
||||
// appears in the last few non-blank lines, the pane has settled to an idle
|
||||
// prompt even if stale scrollback above still contains mid-work text.
|
||||
// Idle-prompt patterns: empty prompt line (no typed text after the char).
|
||||
// Lines like '❯ Working on a task.' have text after the prompt char and
|
||||
// are NOT idle — the prompt is active with input that may look mid-work.
|
||||
const IDLE_PROMPT_PATTERNS = [
|
||||
/^[❯›]\s/, // prompt char + space (with or without typed text)
|
||||
/^[❯›]$/, // prompt char alone (empty input line)
|
||||
/^[❯›]\s*$/, // prompt char + optional whitespace + end-of-line only
|
||||
];
|
||||
|
||||
// Status-bar patterns that ONLY appear when the harness is at its idle
|
||||
// prompt. These are more reliable than the prompt char alone because they
|
||||
// are never rendered during active tool execution.
|
||||
const IDLE_STATUS_BAR_PATTERNS = [
|
||||
/gpt-\d[\d.]* .+ · Context \[/, // Codex model/context footer
|
||||
/⏵⏵ accept edits/, // Claude Code edit-accept bar
|
||||
];
|
||||
|
||||
const IDLE_TERMINAL_COMMANDS = new Set(["zsh", "bash", "sh", "fish", "nu", "tmux"]);
|
||||
@@ -28,12 +35,19 @@ function looksLikeMidWork(paneContent: string): boolean {
|
||||
// Mid-work pattern matched in the 5-line tail. Check whether the pane has
|
||||
// since settled to an idle harness prompt — if so, the match is stale
|
||||
// scrollback, not current working state. Check the last 3 non-blank lines
|
||||
// (covers prompt line + status bar below it).
|
||||
// for either an empty prompt (❯/› alone on a line) or a runtime-specific
|
||||
// idle status bar (Codex model footer / Claude edit-accept bar). These
|
||||
// indicators are only rendered when the harness is at the idle prompt;
|
||||
// prompt chars with text after them (like '❯ Working on a task.') are
|
||||
// NOT treated as idle.
|
||||
const lastNonBlank = lines
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0)
|
||||
.slice(-3);
|
||||
if (lastNonBlank.some((l) => IDLE_PROMPT_PATTERNS.some((p) => p.test(l)))) return false;
|
||||
const hasIdleIndicator =
|
||||
lastNonBlank.some((l) => IDLE_PROMPT_PATTERNS.some((p) => p.test(l))) ||
|
||||
lastNonBlank.some((l) => IDLE_STATUS_BAR_PATTERNS.some((p) => p.test(l)));
|
||||
if (hasIdleIndicator) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -313,6 +313,49 @@ describe("SessionTransport", () => {
|
||||
expect(sendTextSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("send still refuses when prompt char line contains mid-work text (active Claude input)", async () => {
|
||||
seedCanonicalRig();
|
||||
const tmux = mockTmux({
|
||||
capturePaneContent: async () => [
|
||||
"❯ Working on a task.",
|
||||
"",
|
||||
].join("\n"),
|
||||
});
|
||||
const transport = createTransport(tmux);
|
||||
|
||||
const result = await transport.send("dev-impl@my-rig", "hello");
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe("mid_work");
|
||||
});
|
||||
|
||||
// NOTE: r2 flagged '› 1. Yes, continue' as a bypass concern. This content
|
||||
// does NOT match any MID_WORK_PATTERNS (no spinner/Working/esc-to-interrupt)
|
||||
// so looksLikeMidWork was NEVER triggered — pre- or post-fix. This is a
|
||||
// pre-existing gap (trust-prompt pass-through), not a stale-scrollback
|
||||
// regression. Adding trust-prompt detection to MID_WORK_PATTERNS would be
|
||||
// scope expansion per PM constraint #4. Filed as separate named concern in
|
||||
// the handoff. This test documents the current truthful classification.
|
||||
it("send proceeds when prompt char appears in a trust-choice line with no mid-work patterns", async () => {
|
||||
seedCanonicalRig();
|
||||
const sendTextSpy = vi.fn(async () => ({ ok: true as const }));
|
||||
const tmux = mockTmux({
|
||||
capturePaneContent: async () => [
|
||||
"› 1. Yes, continue",
|
||||
"",
|
||||
].join("\n"),
|
||||
sendText: sendTextSpy,
|
||||
});
|
||||
const transport = createTransport(tmux);
|
||||
|
||||
const result = await transport.send("dev-impl@my-rig", "hello");
|
||||
|
||||
// No MID_WORK_PATTERNS match → guard not triggered → send proceeds.
|
||||
// Trust-prompt detection is a separate concern (not stale-scrollback scope).
|
||||
expect(result.ok).toBe(true);
|
||||
expect(sendTextSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("send still refuses when Working footer is present with no idle prompt below it", async () => {
|
||||
seedCanonicalRig();
|
||||
const tmux = mockTmux({
|
||||
|
||||
Reference in new issue
Block a user