Repository navigation
Conversation
…fork
pi's fork emits session_before_fork first, so an extension can cancel
it or ask whether to restore the code to that point (pi's
git-checkpoint and dirty-repo-guard examples). Pi Web's "New session" on
a message copied the branch without asking, so those extensions never
ran.
The wrapper now emits it when a handler listens: `{ cancel: true }`
leaves everything as it was, and a handler's dialog shows in the page
while the fork waits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s lock Before the extensions were bound a handler saw no UI and took its non-interactive branch (dirty-repo-guard cancels silently there), and the hook ran under the session-replacement lock, so while its dialog waited a running source refused Stop and steers. The fork now waits for binding, asks the handlers before taking the lock, and checks the entry and a running ! command again after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReviewThe finding below was checked twice: once by tracing the code and once in a wrapper test. Must fix: the guard fails open after a Stop
pi treats a closed dialog as Fix: renew the controller if it is aborted before emitting. Capture its signal, and treat an abort during the hook as cancelled. The same root cause already affects Follow-up, not a defect of this PR
Checked, fine
|
…for /clone Stop aborts the extension UI controller and only a prompt or reload renewed it, so after a stopped run a handler's dialog rejected at once; pi's runner swallows that, and the fork went through without asking. A Stop while the dialog waited approved it the same way. The hook now renews the controller first and counts a Stop during it as a cancel, as pi's TUI reads a closed dialog. navigate_tree had the same cause for session_before_tree and renews the controller too (a Stop during that hook still cannot undo the move, which pi makes inside navigateTree()). clone now asks session_before_fork at the leaf (position "at"), as pi's /clone does, before the copy's lock; its cancel carries byExtension so the composer says nothing, instead of "Cannot clone an empty or unsaved session". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tree hook's gap A manual /compact after a Stop ran session_before_compact with the controller still aborted, so a handler's dialog rejected at once, as the fork and tree hooks did. The tree comment and note said a Stop cannot land during session_before_tree; it can when a prompt was sent while the dialog waited, and the move still goes through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed 0f419a2 and bd7ac55 for the review above:
Documented, not changed:
|
Problem
pi's
AgentSessionRuntime.fork()emitssession_before_forkbefore it copies anything. A handler can return{ cancel: true }, or ask the user something first.git-checkpointexample offers to restore the code to that point.dirty-repo-guardexample refuses a fork while the repository has uncommitted changes.Pi Web's New session on a user message (
send({ type: "fork" })) copied the branch on disk without asking. Those extensions never ran.Repro on main: an extension with
pi.on("session_before_fork", async (e, ctx) => (await ctx.ui.confirm("Fork this session?", "…")) ? undefined : { cancel: true }). Click New session on a message: it forks at once, with no dialog.Fix
forkemitssession_before_fork({ entryId, position: "before" }), but only when a handler listens, exactly asemitBeforeFork()does.{ cancel: true }returns{ cancelled: true }, and nothing is written.ctx.hasUIas false and takes its non-interactive branch; dirty-repo-guard, for example, cancels silently there.!command are checked again afterwards.session_forkhas no counterpart here: the copy gets its runtime later, when it is opened.Verification
lib/rpc-manager.test.mjs: a cancelled fork writes no file, and the next one goes through.npm test,tsc --noEmit,npm run lint.🤖 Generated with Claude Code