Skip to content

fix(session): start a session whose session_start asks a dialog - #1682

Merged
agegr merged 4 commits into
mainfrom
fix/session-start-dialog-1678
Nov 14, 2026
Merged

agegr merged 4 commits into
mainfrom
fix/session-start-dialog-1678

Conversation

@agegr

@agegr agegr commented Nov 13, 2026

Copy link
Copy Markdown
Owner

Fixes #1678.

An extension whose session_start awaits ctx.ui.confirm/select/input/editor deadlocked a fresh composer. /api/agent/new waited on get_state, get_state waited for extension binding, binding waited for the dialog, and the dialog only reaches the page through the event stream, which the page opens once /api/agent/new has answered. pi's interactive mode starts its UI before session_start for exactly this reason.

Start path

  • get_state still waits for binding, but stops waiting once a blocking extension dialog is pending, and reports the state as it is at that moment. With no dialog, the reply is the same as before (the model after session_start).
  • startRpcSession() uses the same wait. A deferred startup model (a provider an extension registers at session_start, New session silently falls back to another model when the default model is provided by an extension provider #1240) is now switched as the last step of the first binding, not before startRpcSession() returns, so a session on such a model no longer hangs either, fresh or resumed. The switch is skipped when the user or a session_start handler picked another model meanwhile, and on a closing wrapper. A tool change that rebuilds the session during the dialog carries over the pending target, not the stand-in.
  • Client: get_commands (typing /) is the one request a fresh composer sends that waits for binding without an open stream. It now holds the stream while it is out (withSessionStartStream()), so the dialog shows, and closes it again if the composer is still unpromoted. Prompt and ! already open the stream. After a dialog is answered, a fresh composer re-reads the model, level and system prompt.

Shutdown

  • shutdown() waited for binding with no deadline, so idle cleanup could never reap a wrapper stuck in a session_start dialog, and each retry left another one. It now gives open dialogs the cancelled answer and bounds the wait with SESSION_SHUTDOWN_DEADLINE_MS; closingRpcSessionWait() covers both deadlines.
  • A dialog asked after destroy() is refused at once, as after Stop, so a late session_start handler can't wait forever.

Subagents: prepareMcpForRun() now waits for the child's session_start even without an MCP host, and Stop ends that wait.

Not changed: the sidebar's awaiting-input dot for a not-yet-running wrapper, and /api/agent/new with type: "prompt" (the page always sends ensure_session first).

Checks

  • tsc --noEmit, npm run lint, npm test on this branch (3497 tests, 0 failed). The rpc-manager, subagent and hooks suites also pass on Node 22.
  • New tests in lib/rpc-manager-session-start-dialog.test.mjs, hooks/session-start-dialog.test.mjs and a real-SDK case in lib/rpc-manager-deferred-model.integration.test.mjs. The dialog tests time out on the old code.
  • Real-SDK repro with the issue's extension in a temp agent dir: get_state after start went from still pending at 3 s to about 1 ms. Shutdown with the dialog unanswered went from still pending at 8 s to 2 ms.
  • Two independent review rounds; their findings (a stream the Tools rebuild never reopened, a background tab holding SSE, the closing-wait deadline, the deferred model) are fixed in the second and third commits.

🤖 Generated with Claude Code

agegr and others added 3 commits November 14, 2026 01:07
…1678)

An extension whose session_start awaits ctx.ui.confirm/select/input/editor
deadlocked a fresh composer: /api/agent/new waited in get_state for
session_start, while the dialog reached the page only through the event
stream the page opens after that reply. pi's TUI starts its UI first so
session_start may ask one.

- get_state still waits for session_start, but not past a blocking dialog;
  the reply then reports the model and level as they are.
- ensureNewSession() opens the event stream once it has the id, so the
  replayed dialog shows before get_commands, a prompt or a ! command wait
  on session_start.
- Answering a dialog in a fresh composer reads get_state again, since the
  handler may pick the model next.
- shutdown() gives a session_start dialog the cancelled answer and bounds
  the binding wait by the session_shutdown deadline, so idle cleanup can
  reap a wrapper nobody answered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A fresh composer holds the event stream only while get_commands waits on
  session_start (withSessionStartStream()), then lets go of it: no stream
  is left open in a background tab that typed "/", and a session a tool
  change rebuilt gets its own stream with the next "/".
- A deferred startup model (#1240) is switched to as the binding's last
  step, so a session_start dialog no longer holds startRpcSession() (new
  sessions and reopened ones), and what waits for the binding sees it.
- closingRpcSessionWait() covers shutdown()'s two deadlines.
- An extension dialog asked after destroy() is refused at once, as after
  Stop, instead of staying pending forever.
- Answering a dialog in a fresh composer also refreshes the system prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1678)

- A tool change that rebuilds a session whose deferred-model switch a
  session_start dialog still holds rebuilds on that model and level
  (startupModel()), not on the stand-in.
- A model picked before the switch stands, and a level picked meanwhile
  replaces the target's (pendingStartupModel()).
- A closing wrapper skips the switch: it would write to a session being
  shut down.
- A subagent run waits for its session_start before prompting, with or
  without an MCP host (prepareMcpForRun()); Stop ends the wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1678)

Typing "/" in a fresh composer shows a dialog session_start waits on, and
the dialog's layer (z-index 90) sat under the "/" menu (120) while it took
the focus: the user saw only the menu, and Enter answered the hidden dialog
with Cancel. The history, "/" and "@" menus now hide while an extension
dialog or panel is open, their keys pass through, and they come back once
it closes, with their height measured again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@agegr
agegr merged commit 7e2a1c1 into main Nov 14, 2026
2 checks passed
ball6847 added a commit to ball6847/pi-web that referenced this pull request Nov 14, 2026
Upstream: pi 1.5.2, an Agents strip while the main chat holds a subagent
(agegr#1681), a session whose session_start asks a dialog (agegr#1682), the built-in
llama.cpp provider (agegr#1629), tool clocks on the cards, Settings › General's
built-in tool switches (agegr#1611), and 80 fixes.

Conflict resolutions:
- The reveal of the open session's row is now upstream's single effect
  (agegr#1595), extended for the flat list (its rows are keyed by the session,
  not by a group) and for the keyboard chord that asks for the row with the
  focus. Our older reveal effect, its refs and the family map it alone used
  are gone.
- MessageView: upstream's version (tool clocks, a call that never ran, the
  result footer), with our #31 naming back: the Agent card's link opens the
  child's chat, so the prop is `onOpenSubagentChat` and takes only the
  session id.
- ChatWindow and SessionChatPanel: upstream's `onSessionEnded` joins our
  kanban props and the `onOpenSubagentChat` name.
- useAgentSession: upstream's `onSessionEnded` and the `translate` dependency
  join our kanban pick.
- subagents.ts: upstream folded SNAPSHOT_BUILTIN_TOOLS into BUILTIN_TOOLS;
  our kanban exception stays.
- rpc-manager.ts: upstream's llama.cpp extension joins our kanban extension.
- e2e/run.mjs: upstream's directory picker joins our kanban card menu.
- AGENTS.md and the docs: unions — upstream's new files (unread markers,
  user attention, tool clock, the horizontal strip) with our kanban block,
  the flat view, the Quick Open palette and the built-in tools switch.

Verified: tsc 0, lint 0 errors, 3951/3956 tests (the 3 failures are this
machine's: two model-list assertions see the KIMI/MINIMAX/XIAOMI keys as
built-in providers, and the agegr#804 chat-only refusal no longer fires under
pi 1.5.x — it fails with upstream's own lib/rpc-manager.ts too).
@agegr
agegr deleted the fix/session-start-dialog-1678 branch November 14, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A new session hangs when an extension's session_start asks a dialog

1 participant