Repository navigation
fix: keep team tasks and coordinator authority across Claude notifications - #23
Merged
Merged
Conversation
This was referenced Oct 6, 2026
…tions Claude Code submits background-command, subagent and Monitor results as prompts of their own, and UserPromptSubmit fires for them, also mid-turn while it drains queued notifications. The hook payload carries no origin field, so SAB treated these notifications as local terminal input. A worker's active delegated task failed with "A local terminal prompt replaced the delegated worker turn." while the worker kept working. A coordinator lost its owner team turn, so its next sab team send or continue was refused with owner_turn_required. A notification is now recognized by the leading envelope Claude Code writes (<task-notification> or <system-reminder>). It no longer fails the active task or revokes the team turn. A task envelope quoted inside it is never taken as a delivery, an acknowledgement or an identity mismatch. Notifications still reserve team input and start the Claude poller, because they begin or continue a real provider turn, and they are still not mirrored. Typed prompts, typed slash commands, pasted text, Slack channel messages, SAB's own injections, and Codex and Pi prompts keep their existing semantics. Validated: - Full suite passes (555 tests, 7 new), plus shellcheck, .mjs syntax, npm run check and the Pi extension load. - Each of six guard mutations turns the new tests red. - npm run audit fails on GHSA-jqcg-44mw-7w3h (proxy-addr via @modelcontextprotocol/sdk > express). The advisory was published after main's last green CI; this change touches no dependencies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Xs8AVBVrTGfpzUMiCQp2R
SergioTCG
force-pushed
the
fix/team-system-prompt-authority
branch
from
October 6, 2026 11:13
07498d3 to
e750d09
Compare
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.
Summary
Claude Code's own background-command, subagent, and Monitor notifications broke session teams in two ways:
sab team send/continuewas refused withowner_turn_required.SAB now recognizes these notifications by the leading envelope Claude Code writes, and they no longer fail the task or revoke the turn. Typed local input still does both, exactly as before.
Root cause
Claude Code submits background-command completions, background-subagent returns and Monitor events as
<task-notification>prompts, and firesUserPromptSubmitfor each of them. That includes the mid-turn case: while a turn is still running, Claude Code drains a queued notification between tool calls (aqueued_command), and fires the hook at drain time.There is no structural signal to tell these prompts apart from typing:
session_id,transcript_path,cwd,prompt_id,permission_mode,hook_event_name,promptandsession_title.origin.kind: "task-notification"is written after the hook returns. I verified this at hook time for typed prompts, background commands and subagent returns.The prompt hook treated every prompt that was not one of the following as local terminal input:
That input then hit one of two outcomes:
failTeamTaskForSession(session, 'A local terminal prompt replaced the delegated worker turn.')clearTeamTurn(session), which revokes a coordinator's current owner turn.isSystemPromptalready recognized these prompts, but it was only used to skip mirroring.Evidence from the failed batch (Claude team, five workers plus a coordinator)
Workers.
running → failed, with no replies or reports.<task-notification>(for example a backgroundnpm cicompleting) was drained 44 ms to 2.6 s before the failure.Coordinator.
<summary>Monitor event: "…"</summary><event>…</event>) were drained mid-turn during the owner turn.sab team sendcalls were refused twice:Team delegation is available only during a current owner or authorized automatic-continuation turn.Merging. An isolated scratch Claude Code 2.1.291 confirmed the hook timing:
Fix
isProviderInternalPrompt(provider, prompt)indaemon/providers.mjs:<task-notification>or<system-reminder>envelope, with no attributes.unacknowledgedPromptTeamEffect(…)indaemon/team-provider-turn.mjsis a pure decision:null,'fail_task'or'revoke_turn'. Provider notifications, SAB injections and automation echoes returnnull.Deliberately unchanged
reserveTeamInput(session, 'provider')still runs. A notification starts or continues a real provider turn. The worker or coordinator must therefore read as busy, and SAB must not paste new team input into it. The Stop finalizer clears the reservation as before.startPollerstill runs. It is idempotent mid-turn, and a standalone notification turn needs the spinner relay and missing-Stop detection.retireTeamProviderTurnand mirroring are unchanged. Mirroring still usesisSystemPrompt, so notifications are still not posted as terminal typing.awaiting_release) no longer fails the task. Its final becomes another turn report for the same work generation, which is the existing behaviour for later native turns./name; local commands and!bash mode don't fire it at all (verified);<pasted_content>;<channel …>messages;promptSource. MCP channel messages arepromptSource: "system"too, so a source-based exemption could have let a collaborator's Slack turn keep the owner's authority. The envelope test avoids that.SYSTEM NOTIFICATION:continuation is unchanged. It is injected and keeps its existing handling; SAB never writes a provider envelope itself.Tests
New
test/team-provider-notifications.test.mjsadds 7 tests. Its fixtures are prompt shapes captured from Claude Code 2.1.291: a background command, a subagent return, a Monitor event, and a reminder.runningwith its binding intact.consumeCoordinatorDispatch(used bysendandcontinue) andassertCoordinatorTaskControlstill succeed.Each mutation below was applied on its own, then the files were restored byte-identical:
providerInternal(the guard removed)providerInternalValidation
npm test: 555 pass, 0 fail (7 new).npm run check: pass.node --checkon every daemon, channel and scripts.mjs: pass.PI_OFFLINE=1 pi --extension ./pi/sab-extension.ts --list-models: pass.shellcheck: clean.npm run auditfails on a newly published advisory, GHSA-jqcg-44mw-7w3h.proxy-addr2.0.7 via@modelcontextprotocol/sdk → express, a transitive production dependency.main's last green CI (Oct 5), so CI's audit step will fail on this branch and onmainuntil the lockfile is refreshed, as in fix: refresh vulnerable transitive bridge dependencies #20.Rollout notes
hooks/hook.sh, the CLI and the MCP channel are unchanged.mainand restarts the daemon, so merge only when you want to roll.launchctl kickstart -k gui/$(id -u)/si.sergej.claudeslackproxy.runningtask is released as failed;awaiting_releasetasks are kept;dispatchingclaim is never retried.So roll when no task is
dispatching, ideally between batches.CLAUDE.mdasks for a local Claude start/resume smoke test for hook-path changes.🤖 Generated with Claude Code
https://claude.ai/code/session_011Xs8AVBVrTGfpzUMiCQp2R