Skip to content

Default other actors' mid-run asks to follow-up #956

Description

@sentry-junior

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.ts BotConfig / env parse, same surface as other deploy-time bot knobs).
  • Suggested shape: something like 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 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.
  • Router is a UX optimization only. It must not gate credentials or authority (same constraint as the deferred "upfront steering router" note in Audit actor/instruction provenance across plugins #773).

Notes / open questions

  • 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.

Related

Requested by David Cramer via Junior.

--

View Junior Session in Sentry

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions