Skip to content

fix(pi): observe tool events instead of registering a bash tool - #3557

Merged
ellie merged 1 commit into
mainfrom
claude/atuin-issue-3556-6qqcr0
Jul 7, 2026
Merged

fix(pi): observe tool events instead of registering a bash tool#3557
ellie merged 1 commit into
mainfrom
claude/atuin-issue-3556-6qqcr0

Conversation

@ellie

@ellie ellie commented Jul 1, 2026

Copy link
Copy Markdown
Member

The pi extension previously registered its own bash tool via pi.registerTool(createBashTool(...)). Pi only allows one extension to own a tool name, so any other extension that also provides a bash tool (sandboxes, RTK implementations, remote runners) failed to load with:

Tool "bash" conflicts with ~/.pi/agent/extensions/atuin.ts

Rewrite the extension to use pi's extension events instead:

  • tool_call starts an Atuin history entry for bash commands, keyed by tool call ID
  • tool_execution_end closes the entry with the exit code recovered from the result status line ("Command exited with code N", aborted -> 130, timed out -> 124). tool_execution_end is used rather than tool_result because it also fires when another extension blocks the call, so started entries are always closed.

Fixes #3556

Checks

  • I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle
  • I have checked that there are no existing pull requests for the same thing

The pi extension previously registered its own bash tool via
pi.registerTool(createBashTool(...)). Pi only allows one extension to
own a tool name, so any other extension that also provides a bash tool
(sandboxes, RTK implementations, remote runners) failed to load with:

    Tool "bash" conflicts with ~/.pi/agent/extensions/atuin.ts

Rewrite the extension to use pi's extension events instead:

- tool_call starts an Atuin history entry for bash commands, keyed by
  tool call ID
- tool_execution_end closes the entry with the exit code recovered from
  the result status line ("Command exited with code N", aborted -> 130,
  timed out -> 124). tool_execution_end is used rather than tool_result
  because it also fires when another extension blocks the call, so
  started entries are always closed.

Since the extension no longer registers any tool, it coexists with
extensions that replace pi's bash tool, and still records commands no
matter which extension's bash tool executes them. The remaining imports
are type-only, so the extension has no runtime dependency on the
pi-coding-agent package exports.

Fixes #3556
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the pi extension's tool-name conflict by switching from pi.registerTool(createBashTool(...)) to observing tool_call / tool_execution_end events, so the extension coexists with any other bash-tool provider. Exit code recovery is handled via regex parsing of the result status line.

  • The conflict fix is correct and the event-based design is clean; exitCodeFromResult covers the documented status-line formats (exited/aborted/timed-out) with sensible fallbacks.
  • The async tool_call handler awaits startHistory before calling pending.set, which can race against tool_execution_end for fast-completing commands if pi does not guarantee that async handlers are fully awaited before dispatching subsequent events.

Confidence Score: 3/5

Safe to merge for the conflict fix, but the async handler ordering in the tool_call event is worth confirming against pi's event guarantees before shipping.

The core motivation (conflict fix) is solid. The one concern worth addressing is whether pi awaits async tool_call handlers before dispatching tool_execution_end: if it does not, fast bash commands can leave Atuin history entries permanently open. The answer hinges on pi's event semantics, which is not verifiable from this repo alone.

crates/atuin/contrib/pi/atuin.ts — the async pending.set / tool_execution_end ordering

Important Files Changed

Filename Overview
crates/atuin/contrib/pi/atuin.ts Rewrites from a registered bash tool to event observation; fixes the tool-name conflict but introduces a potential race between the async tool_call handler and tool_execution_end for fast commands.
docs/docs/guide/agent-hooks.md Documentation updated to reflect the new event-based approach; accurate and concise.

Reviews (1): Last reviewed commit: "fix(pi): observe tool events instead of ..." | Re-trigger Greptile

Comment on lines +86 to +94
pi.on("tool_call", async (event, ctx: ExtensionContext) => {
if (event.toolName !== "bash") return;

const command = (event.input as { command?: unknown }).command;
if (typeof command !== "string" || command.length === 0) return;

const trackedOperations: BashOperations = {
async exec(command, commandCwd, options) {
const historyId = await startHistory(pi, commandCwd, command);
let exitCode: number | null = null;
const historyId = await startHistory(pi, ctx.cwd, command);
if (historyId) pending.set(event.toolCallId, historyId);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Race: pending.set may lose the run against tool_execution_end

startHistory is awaited inside the tool_call handler, so pending.set only runs after the atuin process exits (up to ATUIN_TIMEOUT_MS = 10 s). If pi's event system does not await async handlers before dispatching subsequent events — which is the norm for JS EventEmitter-style APIs — a fast-completing command can trigger tool_execution_end while startHistory is still in-flight. pending.get returns undefined, the entry is silently skipped, and endHistory is never called, leaving the Atuin record open.

Consider storing a Promise<string | undefined> in pending immediately (before the await) and resolving it in tool_execution_end — or confirm that pi guarantees it awaits all async tool_call handlers before dispatching tool_execution_end.

@ellie
ellie merged commit b6ca28c into main Jul 7, 2026
26 checks passed
@ellie
ellie deleted the claude/atuin-issue-3556-6qqcr0 branch July 7, 2026 00:40
taylordotfish pushed a commit that referenced this pull request Jul 9, 2026
### Bug Fixes

- *(ai)* Dispatch skills registered in the slash command registry
([#3593](#3593))
- *(ci)* Fossier install in scan workflow
([#3485](#3485))
- *(i18n)* Fix typos in Russian localization
([#3575](#3575))
- *(nu)* Use `char -u 1b` for ESC in OSC 133 sequences
([#3530](#3530))
- *(nu)* Suppress error when `ATUIN_HISTORY_ID` is missing in
`pre_prompt` hook
([#3587](#3587))
- *(pi)* Observe tool events instead of registering a bash tool
([#3557](#3557))
- *(pty-proxy)* Set `$SHELL` to the spawned shell
([#3548](#3548))
- *(search)* Fix terminal clearing with latest Ratatui
([#3578](#3578))
- *(sync)* Skip records that fail to decrypt or decode instead of
failing the whole store
([#3569](#3569))
- Atuin hangs when attempting to spawn daemon from Ctrl+R invocation
([#3502](#3502))
- Capture session ID from stream headers rather than final event
([#3531](#3531))
- Doctor resiliency fo runknown platforms + openbsd warning
([#3551](#3551))
- Double input on arrow keys in AI setup prompt on Windows
([#3552](#3552))
- Exclude AI agent commands from zsh-autosuggestions
([#3567](#3567))
- Silence shellcheck SC2016 on literal `$all-user` author filter
- Respect `store_failed` when using daemon
([#3571](#3571))


### Documentation

- Highlight `Ctrl-r` keybinding on docs page
([#3489](#3489))
- Document store purge workflow
([#3544](#3544))
- Fix command example typo in documentation
([#3536](#3536))
- Make commented-out lines in `config.toml` match real defaults
([#3583](#3583))
- Add fish shell cleanup step to uninstall instructions
([#3582](#3582))


### Features

- *(doctor)* Add whether daemon is enabled to `doctor` output
([#3572](#3572))
- *(pty-proxy)* Add `--shell` flag to override the spawned shell
([#3327](#3327))
- Setup fossier to stop bot slop prs
([#3482](#3482))
- Capture command output + expose to new `atuin_output` tool
([#3510](#3510))
- Cache user contexts on load until `/reload`
([#3525](#3525))
- Create database integration tests for atuin-server
([#3514](#3514))
- Add `/model` slash command for changing models
([#3576](#3576))
- Add mcp server for history tools and expand search filters
([#3581](#3581))
- Add status bar with model and usage information
([#3591](#3591))


### Miscellaneous Tasks

- *(rustdoc)* Fix Rustdoc warnings
([#3585](#3585))
- *(warnings)* Fix compile warnings with latest dependencies
([#3586](#3586))
- Vouch for all existing contributors
([#3486](#3486))
- Update GitHub app token format
- Update to Rust 1.96.1
([#3568](#3568))
- Adopt `derive_more` to reduce boilerplate across the codebase
([#3573](#3573))


### Performance

- *(search)* Scan history by recency until N unique
([#3553](#3553))
- Add `synchronous(Normal)` + `optimize_on_close` to record store SQLite
([#3577](#3577))
- Remove unnecessary clones in a hot path
([#3580](#3580))


### Refactor

- Implement `From<sqlx::Error>` and clean up `fix_error`
([#3484](#3484))
- Pull `fn into_utc` into `atuin-server-database` crate
([#3487](#3487))
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.

Incompatibility with RTK implementation in Pi harness

1 participant