You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When actor A owns an active run and actor B @-mentions Junior in the same thread, treat B's message as a follow-up (own turn after the active one) by default, not as mid-run steering into A's turn. This shrinks the multi-actor surface that drives confused-deputy failures: if B cannot steer A's run, B's text does not join run.actors or shape A's credentialed work.
Current behavior
Terminology: steering interrupts the active turn at the next safe boundary (messages arriving together may batch into that turn); follow-up waits for the active turn to finish (TERMINOLOGY.md).
Classification is mention-shaped, not actor-shaped. In packages/junior/src/chat/runtime/slack-runtime.ts (decideSteeringMessage), activeRequest || isMention → mode "interrupt" (steered into the live run); otherwise mode "defer" (left pending for a later worker slice after the active answer). The author is never compared to the current run actor.
Accepted interrupts are injected with agent.steer(...) and steeringMode: "all" (packages/junior/src/chat/agent/index.ts), so every drained interrupt batches into one steering drain. Junior does not use Pi followUp() for this path.
Pre-turn mailbox batching also folds earlier explicit mentions from any author into the latest turn as instruction provenance (reply-executor.ts batched parked input), so those authors join run.actors.
Gap
Both steering and a separate ask are valid UX, but defaulting every explicit mention to interrupt makes multi-actor runs common and is one of the trickiest thread UX failure modes today. It feeds the confused-deputy class tracked in #773 (cross-actor text under the run actor's credentials / run.actors expansion). Related interim hardening: #781.
#773 currently prefers keeping cross-actor messages as attributed context with point-of-action authority checks, and previously rejected always-queue-cross-actor-asks mainly because same-thread corrections ("stop, that's wrong") would pay active-run latency. This issue is the complementary product default: prefer isolation for other people's asks, accept the correction-latency tradeoff unless a later router reopens steering for clearly corrective messages.
Deployments should also be able to opt into the old behavior or pick a different default without a code fork.
Proposal
Phase 1 — deterministic default
If the inbound actor ≠ the active run's credential/instruction actor, default the message to follow-up (own turn after the active turn completes), not mid-run steering.
Same-actor mid-run mentions keep current interrupt/steering behavior.
Contiguous-actor follow-up batching: only batch consecutive follow-ups from the same actor into one next turn. Do not merge Alice+Bob deferred asks into a single multi-actor follow-up the way today's interrupt drain batches all accepted steers together (steeringMode: "all").
Keep authority out of any classifier: this phase is pure actor-id comparison against the active run actor (runtime-owned identity, not prompt text).
Configuration
Expose the policy as a botConfig option (wired through packages/junior/src/chat/config.tsBotConfig / env parse, same surface as other deploy-time bot knobs).
Suggested shape: something like crossActorMidRunMode: "follow_up" | "steer" | "router" with default "follow_up".
steer — preserve today's interrupt-everyone behavior for deployments that want collaborative mid-run steering.
router — reserved for Phase 2; until the router ships, treat as follow_up or refuse at config parse time.
Operators should be able to control the default without code changes; keep the option deploy/config-scoped (not per-thread or per-user) unless a later issue expands it.
Phase 2 — optional steering router (future)
After the default is in place, add a router/classifier that can promote a non-current-actor message to steering when it is clearly a correction/contribution to the active task (or demote an ambiguous same-actor message to follow-up).
Enable via the same botConfig option (router mode), not a separate hidden flag.
Whether "follow-up" should stay as today's mailbox defer path (pending until active answer delivers) or move onto Pi followUp() once actor-scoped batching exists.
How hard-stop / opt-out messages from non-owners should behave under the stricter default (likely still need a fast path that aborts without adopting B as an instruction actor).
Interaction with Audit actor/instruction provenance across plugins #773 Track 2 point-of-action checks: even with this default, same-actor multi-message runs and any router-promoted cross-actor steers still need provenance + authority policy.
Exact config name / env var naming; keep it on BotConfig unless research finds a better home under ChatConfig.
When actor A owns an active run and actor B @-mentions Junior in the same thread, treat B's message as a follow-up (own turn after the active one) by default, not as mid-run steering into A's turn. This shrinks the multi-actor surface that drives confused-deputy failures: if B cannot steer A's run, B's text does not join
run.actorsor shape A's credentialed work.Current behavior
TERMINOLOGY.md).packages/junior/src/chat/runtime/slack-runtime.ts(decideSteeringMessage),activeRequest || isMention→ mode"interrupt"(steered into the live run); otherwise mode"defer"(left pending for a later worker slice after the active answer). The author is never compared to the current run actor.agent.steer(...)andsteeringMode: "all"(packages/junior/src/chat/agent/index.ts), so every drained interrupt batches into one steering drain. Junior does not use PifollowUp()for this path.reply-executor.tsbatched parked input), so those authors joinrun.actors.Gap
Both steering and a separate ask are valid UX, but defaulting every explicit mention to interrupt makes multi-actor runs common and is one of the trickiest thread UX failure modes today. It feeds the confused-deputy class tracked in #773 (cross-actor text under the run actor's credentials /
run.actorsexpansion). Related interim hardening: #781.#773 currently prefers keeping cross-actor messages as attributed context with point-of-action authority checks, and previously rejected always-queue-cross-actor-asks mainly because same-thread corrections ("stop, that's wrong") would pay active-run latency. This issue is the complementary product default: prefer isolation for other people's asks, accept the correction-latency tradeoff unless a later router reopens steering for clearly corrective messages.
Deployments should also be able to opt into the old behavior or pick a different default without a code fork.
Proposal
Phase 1 — deterministic default
steeringMode: "all").Configuration
botConfigoption (wired throughpackages/junior/src/chat/config.tsBotConfig/ env parse, same surface as other deploy-time bot knobs).crossActorMidRunMode: "follow_up" | "steer" | "router"with default"follow_up".follow_up— Phase 1 behavior (recommended default).steer— preserve today's interrupt-everyone behavior for deployments that want collaborative mid-run steering.router— reserved for Phase 2; until the router ships, treat asfollow_upor refuse at config parse time.Phase 2 — optional steering router (future)
botConfigoption (routermode), not a separate hidden flag.Notes / open questions
deferpath (pending until active answer delivers) or move onto PifollowUp()once actor-scoped batching exists.BotConfigunless research finds a better home underChatConfig.Related
TERMINOLOGY.md— steering vs follow-uppackages/junior/src/chat/config.ts—BotConfig/ env parse surfacepackages/junior/src/chat/runtime/slack-runtime.ts— interrupt vs defer classificationpackages/junior/src/chat/agent/index.ts—steer+steeringMode: "all"Requested by David Cramer via Junior.
--
View Junior Session in Sentry