Repository navigation
presence: show the model name only (drop the provider prefix) - #59
Merged
Merged
Conversation
Discord activity read "deepseek/deepseek-v4.1-flash-1017"; now it reads
"deepseek-v4.1-flash-1017". split("/").pop() handles a bare model id
(no provider segment) by returning it unchanged.
frolic
pushed a commit
that referenced
this pull request
Oct 17, 2026
Fixes a steering bug that the pi 1.3.0 bump widened. Base merged, so this is now a clean single-concern PR on `main` — the diff is the fix commit plus its test, nothing else. Main has moved since this branch was cut (#59, "presence: show the model name only"), which touches `createAgentPool.ts` in the same file. Merged rather than rebased, per the repo's convention; the merge was clean and #59's change is intact. ## The bug pi has two busy flags and they flip at different points: - `session.isStreaming` — pi's `_isAgentRunActive` - `session.agent.state.isStreaming` — the agent loop's flag, which guards `activeRun` `createAgentPool.handle()` gated steering on the **agent** flag. But pi 1.2.0 moved the **session** flag to the first line of its run-start sequence, which then awaits the compaction check and the `before_agent_start` handlers; the agent flag only becomes true inside the later `agent.prompt()` call. So during that window a message arriving from Discord found the agent flag `false`, the harness concluded nothing was in flight, and it submitted a plain `prompt(text)` with no `streamingBehavior`. pi checks *its* flag, sees busy, and throws: ``` Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message. ``` `handle()` catches that and posts it via `sender.sendError` — so the user gets an error line and **their message is never processed**. On 0.84.4 the two flags flipped one statement apart, so this was effectively unreachable; on 1.3.0 the window spans the whole run-preparation, which is exactly when someone fires off a quick correction. `evict()` read the same flag for its busy check. Benign there, but wrong for the same reason — a just-started session could be evicted. ## Reproducing it The window is normally sub-second, so I widened it deterministically by registering a ~2s `before_agent_start` handler in a throwaway agent home, then probing both flags while a run was starting: ``` PASS observed the gap: session busy while agent.state.isStreaming is false PASS session.isStreaming === true during the gap PASS agent.state.isStreaming === false during the gap PASS a plain prompt during the gap is REJECTED (today's harness path) PASS the rejection is the 'already processing' error PASS a steer during the gap is ACCEPTED (the fixed path) PASS the steered message landed in the session ``` Both flags read and both prompt paths exercised in the *same* window, so this is the bug and the fix landing on the same measurement, not two separate runs. ## The fix Read `session.isStreaming` at all three sites, via a small `isSessionBusy()` helper that carries the explanation — this is a trap someone will otherwise walk back into. `handle()` also now reads the flag once and uses that value for both the option it builds and the branch it takes; those must agree or the prompt carries the wrong disposition. **Why this can't regress anything:** the session flag is cleared in `_emitAgentSettled()`, which runs after `agent.prompt()` and the post-run loop finish — so it is strictly *wider* than the agent flag, never narrower. Every case that previously steered still steers, and the only newly-steered case is the window that previously errored. ## What I deliberately didn't do - **Catch-and-resubmit-as-steer.** Tempting as a belt-and-braces, but pi has no typed error class for this — all five "already processing" throws are bare `new Error()` with different messages, so it would be a string match against prose. That is brittle and would mask genuine "already processing" bugs. Reading the authoritative flag makes the error unreachable on this path instead. - **`wakeUp()` keeps a plain `prompt()`.** Its three callers (`thread.ts` on a brand-new thread, `recoverActive` on a fresh process, `handleClear` after `dropEntry`) all target a session that is fresh or just dropped, so it can't be busy. If a caller is ever added that can hit a warm busy session, it needs the same gate — noted in the code. ## Tests Added `src/agent/buildPromptOptions.test.ts` (4 cases) pinning the `isBusy` → `streamingBehavior: "steer"` pairing, which is the contract this bug violated. `buildPromptOptions` was the one piece of this change with a testable seam — the pool itself needs a live discord.js client and a full pi session to construct. `bun run typecheck` clean, `bun test` 106/106.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Frolic's ask: the bot's Discord activity showed
deepseek/deepseek-v4.1-flash-1017; trim it to everything after the last slash →deepseek-v4.1-flash-1017.split("/").pop()returns the whole string unchanged when the model id has no provider segment, so a bare id still renders correctly.currentPresenceModel→currentPresenceText(it holds presence text, not a provider/model pair) and updated the doc comment.tsc --noEmitclean.Not covered by a test — there's no existing test for presence, and this is a one-line string transform. Say the word if you'd like one.