Skip to content

fix(sessions): close four prompt-lifecycle holes - #851

Merged
RonenMars merged 4 commits into
mainfrom
fix/prompt-lifecycle-guards
Sep 10, 2026
Merged

fix(sessions): close four prompt-lifecycle holes#851
RonenMars merged 4 commits into
mainfrom
fix/prompt-lifecycle-guards

Conversation

@RonenMars

Copy link
Copy Markdown
Owner

Four holes in how the streamer opens, answers and retires prompts, one commit each.

Escape binding — #848

POST /raw-key exempted escape from all prompt arbitration, so a card's Cancel was a blind \x1b that could interrupt the running turn after the card's prompt had closed.
An escape that names a promptId now gets the same arbitration as every other action: a stale or unfocused prompt is refused with 409 raw_key_stale and nothing is written.
A bound escape retires its prompt as cancelled and clears its pending entry, and an escape without a promptId stays blind.

Stale permission entry — #849

Two paths left a permission entry behind that kept POST /input refusing composer text after the client had taken the card down.
The contract answer route skipped the cleanup the legacy route's gateClosed() does after a failed freshness scrape, and a gate whose OSC fired before its options painted was stored with no promptId, which isLive() reads as live forever.
Both now clear the entry when no gate is painted and keep refusing while one is, so composer text never lands on a gate the detector has not announced yet and #703 stays intact.

Answered question re-opening — #724

The contract answer and raw-key Enter paths deleted a question's dedupe key along with its pending entry, so the still-painted menu re-minted as a fresh open prompt.
processJsonlQuestions also minted from any AskUserQuestion tool_use without checking whether it had been answered, and its 60-second timer is the phantom's measured lifetime.
Both paths now keep the key until the menu leaves the screen, a late JSONL flush of an answered question is skipped, and exit clears the key so the menu shows again on resume.

Shell-prompt gates — #850

permissionGateStillOpen read only the Claude box scraper, so all three answer routes refused every card raised by detectShellPrompt on the Claude provider.
That refusal is what left the #849 entry behind in the RonenMars/threadbase-mobile#954 lockout.
The check now accepts either detector's gate whose content key matches, and the painted check behind #849 counts shell prompts too.

Verification

  • Full suite on the final tree: 3043 passed, 5 skipped.
  • Every change's tests go red with its source reverted, failing for the defect's own reason.

Client side

Closes #848
Closes #849
Closes #724
Closes #850

POST /raw-key exempted escape from all prompt arbitration, so a card's Cancel was a blind \x1b.
When the gate had already closed, that Escape landed at Claude's prompt and interrupted the turn the user was waiting on.
Escape now gets the same binding as every other action whenever the caller names a promptId: a stale or unfocused prompt is refused with 409 raw_key_stale and nothing is written.
A bound Escape retires its prompt as cancelled and clears its pending entry, so dismissing a card no longer leaves the composer refused.
Escape without a promptId stays exactly as blind as before, for the raw keyboard and the interrupt action.
POST /input refuses text while a permission entry is pending, and two paths left an entry behind with nothing to retire it.
The contract answer route returned prompt_cancelled from a failed freshness scrape without clearing the entry, unlike the legacy route's gateClosed(), so /input read the retained cancelled record as an open prompt for up to ten minutes.
A gate whose OSC fired before its options painted was stored with no promptId, and isLive() treated that entry as live forever.
Both now clear the entry when no gate is painted at all and keep refusing while one is, so text never lands on a gate the detector has not announced yet.

Closes #849
After a question was answered, the menu that stayed painted, or a JSONL flush landing after the answer, minted a new open prompt for a question already settled.
The contract answer and raw-key Enter paths deleted the question's content key along with its pending entry, so the next scrape of the still-painted menu no longer deduped; the legacy route keeps the key and never had this.
Both paths now keep the key until the menu leaves the screen, and a new host occurrence with identical content still opens, matching the permission path's identity rule.
processJsonlQuestions minted from any AskUserQuestion tool_use without checking whether it had been answered, and its 60-second timer is the lifetime the live probe measured; a flush matching an answered question's key is now skipped.
Session exit clears the kept key so the same menu shows again on resume.

Closes #724
permissionGateStillOpen checked only the Claude box scraper, but pty-manager also raises cards from detectShellPrompt (`read -p "[y/N]"`, "press Enter"), which is never a Claude box.
On the Claude provider all three answer routes therefore refused every answer to such a card as closed, so a shell prompt's Continue could never write its \r.
The check now accepts either detector's gate whose content key matches the pending one.
The painted check behind the stale-entry cleanup had the same blindness, so it could clear an entry while a [y/N] prompt was still up and let composer text answer it; it now counts shell prompts too.

Closes #850
@RonenMars
RonenMars force-pushed the fix/prompt-lifecycle-guards branch from 98c5340 to b9d2dc6 Compare September 10, 2026 15:47
@RonenMars
RonenMars merged commit 33e1c4b into main Sep 10, 2026
10 checks passed
@RonenMars
RonenMars deleted the fix/prompt-lifecycle-guards branch September 10, 2026 15:57
@RonenMars

Copy link
Copy Markdown
Owner Author

🎉 This PR is included in version 1.89.5 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment