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
Junior is the core agent. It owns every Conversation, Turn, and Run. The
Conversation API, local CLI, child work, task work, dispatch work, and providers
all use this agent. Slack adds input, context, and Delivery to a Junior
Conversation. It does not create another agent runtime.
Intent
Use one small interface from stored input through each Run. Use the terms
Conversation, Source, Actor, Location, Delivery, and Destination with one meaning
each. Remove fields that copy the same fact or join facts that can vary
independently.
The types are the main design. Behavior must follow them.
Core model
Input and Source
Every Inbound message has one required Source.
Source says what produced that input. Examples include a Slack message, web
input, local CLI input, Resource event, Scheduled task, Event task, Plugin
dispatch, and Agent invocation.
Source uses kind to say what produced the input. Do not use platform because many
Source kinds are not providers.
Provider Source kinds may keep identifiers for their input, such as a Slack
message timestamp. Source does not contain the Conversation Location.
When an input starts a Turn, the worker saves its Source and Actor on that
Turn. Other batched or steering inputs retain their own Source.
Resume restores the Source and Actor saved for the Turn.
Conversation and Location
Every Conversation belongs to Junior.
A Conversation may have one parent Conversation, identified by parentConversationId.
A Conversation has zero or one complete Location stored in SQL.
Location names one place outside Junior where the Conversation can be
delivered. For Slack, it identifies the workspace, channel, and thread when a
thread exists. Channel-level Slack Locations may omit threadTs.
The parent relation and Location are independent. A child does not copy its
parent Location.
A Run carries the Conversation Location when the agent or tools need it.
Location does not allow output to be sent. Conversation visibility is
separate.
Turn
A Turn stores the Source and Actor selected from the input that started it.
The Conversation always stores each completed assistant Message.
Do not store a per-message or per-Turn delivery choice. It would add state to
mailbox, Turn checkpoint, and resume contracts before we have behavior that
needs the same input kind to make different choices.
If that behavior becomes necessary, add an explicit choice for the proven
case instead of keeping one on every Message and Turn now.
Run and Delivery
Delivery is an optional function that sends Run output to the Conversation
Location.
Before every new or resumed Run, the work owner supplies Delivery when its
work must return to a provider.
Slack input gets Slack Delivery.
Web or local input gets no provider Delivery, including web input in a
Slack-linked Conversation.
A Resource event gets Delivery from the Conversation Location when it has
one.
A Scheduled task, Event task, or Plugin dispatch uses its explicit
Destination to create the Conversation Location. The dispatch work owner
supplies Delivery for that Location.
An Agent invocation gets no Delivery. Its Run may carry the parent Location
when tools need it.
Delivery is created for the Location. It does not contain another copy of
Location.
Source and Delivery are independent. Source says what caused the Run. The
work owner decides whether to supply Delivery.
Core does not select a provider from Source or Actor.
publishExternally is a legacy stored mailbox and Turn checkpoint field.
Keep it only while deployed readers require it. Current runtime contracts do
not use it.
Destination
Destination is an explicit target for output or a side effect.
A feature may use Destination before a Conversation exists. A Scheduled task
or Event task can use a Slack Destination to choose where it creates a new
Conversation. That target then becomes the Conversation Location.
A feature may also use Destination when it deliberately targets a place other
than the Conversation Location.
Destination is not part of the core agent contract.
Final interface
These are the relevant fields, not complete type definitions:
The final AgentRun has no Destination, destinationVisibility, or publishExternally. It carries Location once. Source and Delivery do not repeat
it. Do not add a routing object, wrapper, registry, or alias for these values.
Required behavior
Input
Conversation Location
Run Delivery
Slack message
Slack thread
Slack
Local CLI input
none
none
Web input in a new Junior Conversation
none
none
Web input in a Slack-linked Conversation
Slack thread
none
Resource subscription wake
existing Conversation Location
provider Delivery for that Location, when present
Scheduled task, Event task, or Plugin dispatch targeting Slack
Slack Location created from the explicit Destination (threadTs optional for channel-level targets)
Slack
Agent invocation
parent Location when tools need it
none
System wakes must start a Turn from Conversation Location + saved Source/Actor +
mailbox input. They must not invent a Slack Message/Thread, and they must not
require threadTs when the Location is channel-level.
Add or extend the main integration scenarios for these behaviors. Do not add one
test for each implementation branch.
Also cover:
Slack replies stay in the same thread when Location has threadTs.
Channel-level Slack Locations (no threadTs) still accept system wakes and can
deliver without a synthetic webhook Message.
A paused or authorization resume restores the saved Source, Actor, and
Conversation Location. The work owner rebuilds the same Delivery.
Batched and steering inputs keep their own Sources.
A child Conversation stores parentConversationId and does not copy the
parent Location. Its Run may read the parent Location without Delivery.
Storing a completed assistant Message does not require Delivery.
Current conflicts
Resource events have a first-class Source after ref(chat)!: add Resource event Source #1730. Scheduled tasks,
Event tasks, Plugin dispatches, and Agent invocations still need their own
Source kinds through every Run.
Scheduled and Event tasks build a Slack Source even though the task or event
caused the Turn. Event-task ingest often builds that Slack Source without threadTs for channel-level Destinations.
Conversation Location is still copied through Destination and sessionSource.
The current Run contract copies Location into Source and Delivery, which
requires cross-field consistency checks.
Conversation-only web work uses Delivery to store the assistant Message.
Conversation storage and provider Delivery are not yet independent.
Slack system wakes still depend on slack-resource-event.ts: a temporary
bridge that invents Message/Thread from Conversation routing. That bridge
requires threadTs. When Location/Source has none (common for event-task and
channel-level bindings), resolveSlackResourceEventThread cannot deliver.
JUNIOR-90 production RCA
(2026-08-27 → ongoing until 0.188.0 is deployed): after #1706 (fix(resource-events): let conversations own watch routing, shipped in 0.186.0 via junior-prod #353), plain
resource-event wakes no longer carry Slack thread metadata on the mailbox row.
Delivery must come from Conversation routing. Missing threadTs caused Conversation … is missing a Slack thread for resource-event delivery, then
worker retries and dead-letter noise (~44k handled errors; almost all non-Zod
prod volume on POST /api/internal/agent/continue, mostly agent-dispatch:dispatch_*). Delivery was already impossible; the throw only
multiplied Sentry/worker churn. Metric-alert “resolved” on that series was
dynamic flapping, not a fix.
#1721 (fix(chat): ack-drop resource wakes missing Slack thread, in 0.188.0) is a temporary
stopgap: return undefined, warn conversation.work.resource_event.missing_thread, ack once. It stops the
retry storm. It does not restore automation for threadless Slack
Locations. Prod was still on 0.187.0 when this was written; deploy is junior-prod #355.
Real fix remains deleting the synthetic bridge under this issue so Slack
system turns start from Conversation Location without inventing a thread.
Fields and behavior to remove
Source.platform as the Source discriminant. Replace it with Source kind.
Source.location and Delivery.location. Use AgentRun.location once.
Conversation destination when it names the Conversation Location.
AgentRun.destination.
The invented local Destination for Conversation API work.
sessionSource. Save Source with the input and Turn. Save Location on the
Conversation.
turn-session-routing. Remove the mixed Source, Location, and Destination
interface after each owner reads its own fact.
AgentRun.publishExternally and the per-message delivery choice. Each work
owner supplies Delivery directly. Keep the old Redis field only for deployed
readers during rollout.
Delivery callbacks that only store Conversation Messages. Conversation
storage is part of the core Turn lifecycle.
packages/junior/src/chat/task-execution/slack-resource-event.ts and the
plain-resource synthetic Message/Thread branch in Slack work. Remove once
Slack system turns start from Conversation Location + mailbox input (see
JUNIOR-90 / fix(chat): ack-drop resource wakes missing Slack thread #1721 stopgap above).
Keep Destination only where a feature owns an explicit target. Keep compatibility
only for stored SQL, Redis, queue, or public data that can exist during a
deployment.
Ownership
Junior owns Conversation storage, Turn state, model execution, tools, agent
history, completed assistant Messages, compaction, handoff, sandbox state,
cancellation, timeouts, and the common pause and resume contract.
The Conversation API owns access checks, web input, mailbox admission, and API
errors.
Input owners choose Source and Actor for the Inbound message they create. The
worker saves those facts on the Turn.
Work owners decide whether a Run gets Delivery. Providers build their
Delivery function. They do not own another model loop or runtime.
Migration rules
Make one small behavior or contract change per pull request.
Use hard cutovers for internal TypeScript names and signatures.
Keep compatibility only for stored SQL, Redis, queue, or public data that can
exist during deployment.
Keep old SQL column and table names behind the storage adapter until deployed
readers and writers use the new contract.
Do not add live aliases or scan old storage on every message.
Each removal TODO names its owner, the old field or behavior, and the condition
that makes removal safe.
Do not add a provider registry, adapter framework, manager, dependency bag, or
parallel runtime.
Do not treat ack-drop / warn-and-skip of undeliverable system wakes as the end
state for threadless Slack Locations. That only bounds failure while the plain
system Turn path is missing.
Delivery plan
Complete the stored Conversation Location. For Slack, include teamId, channelId, and threadTs when present. Treat missing threadTs as a valid
channel-level Location, not an error.
Give every Inbound message a complete Source with a kind. Resource event
Source landed in ref(chat)!: add Resource event Source #1730. Add first-class Source kinds for Scheduled tasks,
Event tasks, Plugin dispatches, and Agent invocations. Stop rewriting those
wakes as Slack/local Source stand-ins.
Save Source and Actor on the Turn. Restore them for every resume.
Make each work owner supply optional Delivery before every new or resumed
Run. Slack input supplies Slack Delivery. Web input supplies none. Resource
events use the Conversation Location. Scheduled tasks, Event tasks, and
Plugin dispatches use their explicit Destination to create the Conversation
Location, then supply Delivery for it. Agent invocations supply none. Slack
Delivery must work for Locations with or without threadTs
without synthesizing chat-adapter Message/Thread for system wakes. Delete slack-resource-event.ts when that entry exists; retire the fix(chat): ack-drop resource wakes missing Slack thread #1721 ack-drop
branch with it.
Store completed assistant Messages in the core Turn lifecycle. Keep provider
Delivery separate.
Move remaining place readers from Destination and sessionSource to
Location. Remove turn-session-routing.
Remove AgentRun.destination, destinationVisibility, the fake local
Destination, and AgentRun.publishExternally.
Migrate legacy durable field names after deployed readers and writers use
the final contract.
Outcome
The work is complete when:
Every caller uses one Junior agent runtime.
Every Inbound message has a Source that says what produced it.
Every Turn saves its Source and Actor.
No Inbound message or Turn stores a delivery choice.
Storage and the Run interface agree that a Conversation has at most one
Location.
AgentRun carries that Location once.
Source does not contain Conversation Location.
Delivery does not contain Conversation Location.
The agent has no required Destination and no invented local provider model.
Provider Delivery is explicit and independent from Source.
Conversation storage does not depend on provider Delivery.
Slack system wakes (resource events, event tasks, and peers) start without a
fake Message/Thread and without requiring threadTs on channel-level
Locations.
slack-resource-event.ts and the JUNIOR-90 ack-drop stopgap are gone because
the plain path makes them unnecessary.
A maintainer can follow the types from input through Conversation, Turn, and
Run without translating between duplicate names.
The change removes more concepts and branches than it adds.
Junior is the core agent. It owns every Conversation, Turn, and Run. The
Conversation API, local CLI, child work, task work, dispatch work, and providers
all use this agent. Slack adds input, context, and Delivery to a Junior
Conversation. It does not create another agent runtime.
Intent
Use one small interface from stored input through each Run. Use the terms
Conversation, Source, Actor, Location, Delivery, and Destination with one meaning
each. Remove fields that copy the same fact or join facts that can vary
independently.
The types are the main design. Behavior must follow them.
Core model
Input and Source
input, local CLI input, Resource event, Scheduled task, Event task, Plugin
dispatch, and Agent invocation.
kindto say what produced the input. Do not useplatformbecause manySource kinds are not providers.
message timestamp. Source does not contain the Conversation Location.
Turn. Other batched or steering inputs retain their own Source.
Conversation and Location
parentConversationId.delivered. For Slack, it identifies the workspace, channel, and thread when a
thread exists. Channel-level Slack Locations may omit
threadTs.parent Location.
separate.
Turn
mailbox, Turn checkpoint, and resume contracts before we have behavior that
needs the same input kind to make different choices.
case instead of keeping one on every Message and Turn now.
Run and Delivery
Location.
work must return to a provider.
Slack-linked Conversation.
one.
Destination to create the Conversation Location. The dispatch work owner
supplies Delivery for that Location.
when tools need it.
Location.
work owner decides whether to supply Delivery.
publishExternallyis a legacy stored mailbox and Turn checkpoint field.Keep it only while deployed readers require it. Current runtime contracts do
not use it.
Destination
or Event task can use a Slack Destination to choose where it creates a new
Conversation. That target then becomes the Conversation Location.
than the Conversation Location.
Final interface
These are the relevant fields, not complete type definitions:
The final
AgentRunhas no Destination,destinationVisibility, orpublishExternally. It carries Location once. Source and Delivery do not repeatit. Do not add a routing object, wrapper, registry, or alias for these values.
Required behavior
threadTsoptional for channel-level targets)System wakes must start a Turn from Conversation Location + saved Source/Actor +
mailbox input. They must not invent a Slack Message/Thread, and they must not
require
threadTswhen the Location is channel-level.Add or extend the main integration scenarios for these behaviors. Do not add one
test for each implementation branch.
Also cover:
threadTs.threadTs) still accept system wakes and candeliver without a synthetic webhook Message.
Conversation Location. The work owner rebuilds the same Delivery.
parentConversationIdand does not copy theparent Location. Its Run may read the parent Location without Delivery.
Current conflicts
Event tasks, Plugin dispatches, and Agent invocations still need their own
Source kinds through every Run.
caused the Turn. Event-task ingest often builds that Slack Source without
threadTsfor channel-level Destinations.sessionSource.requires cross-field consistency checks.
Conversation storage and provider Delivery are not yet independent.
slack-resource-event.ts: a temporarybridge that invents Message/Thread from Conversation routing. That bridge
requires
threadTs. When Location/Source has none (common for event-task andchannel-level bindings),
resolveSlackResourceEventThreadcannot deliver.(2026-08-27 → ongoing until 0.188.0 is deployed): after
#1706 (
fix(resource-events): let conversations own watch routing, shipped in 0.186.0 viajunior-prod #353), plain
resource-event wakes no longer carry Slack thread metadata on the mailbox row.
Delivery must come from Conversation routing. Missing
threadTscausedConversation … is missing a Slack thread for resource-event delivery, thenworker retries and dead-letter noise (~44k handled errors; almost all non-Zod
prod volume on
POST /api/internal/agent/continue, mostlyagent-dispatch:dispatch_*). Delivery was already impossible; the throw onlymultiplied Sentry/worker churn. Metric-alert “resolved” on that series was
dynamic flapping, not a fix.
fix(chat): ack-drop resource wakes missing Slack thread, in 0.188.0) is a temporarystopgap: return
undefined, warnconversation.work.resource_event.missing_thread, ack once. It stops theretry storm. It does not restore automation for threadless Slack
Locations. Prod was still on 0.187.0 when this was written; deploy is
junior-prod #355.
system turns start from Conversation Location without inventing a thread.
Fields and behavior to remove
Source.platformas the Source discriminant. Replace it with Sourcekind.Source.locationandDelivery.location. UseAgentRun.locationonce.destinationwhen it names the Conversation Location.AgentRun.destination.destinationVisibility. Read Conversation visibility.sessionSource. Save Source with the input and Turn. Save Location on theConversation.
turn-session-routing. Remove the mixed Source, Location, and Destinationinterface after each owner reads its own fact.
AgentRun.publishExternallyand the per-message delivery choice. Each workowner supplies Delivery directly. Keep the old Redis field only for deployed
readers during rollout.
storage is part of the core Turn lifecycle.
packages/junior/src/chat/task-execution/slack-resource-event.tsand theplain-resource synthetic Message/Thread branch in Slack work. Remove once
Slack system turns start from Conversation Location + mailbox input (see
JUNIOR-90 / fix(chat): ack-drop resource wakes missing Slack thread #1721 stopgap above).
Keep Destination only where a feature owns an explicit target. Keep compatibility
only for stored SQL, Redis, queue, or public data that can exist during a
deployment.
Ownership
history, completed assistant Messages, compaction, handoff, sandbox state,
cancellation, timeouts, and the common pause and resume contract.
errors.
Delivery, and Slack errors.
worker saves those facts on the Turn.
Delivery function. They do not own another model loop or runtime.
Migration rules
exist during deployment.
readers and writers use the new contract.
that makes removal safe.
parallel runtime.
state for threadless Slack Locations. That only bounds failure while the plain
system Turn path is missing.
Delivery plan
teamId,channelId, andthreadTswhen present. Treat missingthreadTsas a validchannel-level Location, not an error.
parentConversationIddirectly on Conversation. Keep theparent relation independent from Location. Do not copy the parent Location.
Done in ref(chat): Store parent on Conversation directly #1718.
AgentRun. Remove nested Location from Sourceand Delivery. Temporary Run Location step landed in ref(chat): pass Conversation Location into agent runs #1715; finish by removing
nested copies.
kind. Resource eventSource landed in ref(chat)!: add Resource event Source #1730. Add first-class Source kinds for Scheduled tasks,
Event tasks, Plugin dispatches, and Agent invocations. Stop rewriting those
wakes as Slack/local Source stand-ins.
Run. Slack input supplies Slack Delivery. Web input supplies none. Resource
events use the Conversation Location. Scheduled tasks, Event tasks, and
Plugin dispatches use their explicit Destination to create the Conversation
Location, then supply Delivery for it. Agent invocations supply none. Slack
Delivery must work for Locations with or without
threadTswithout synthesizing chat-adapter Message/Thread for system wakes. Delete
slack-resource-event.tswhen that entry exists; retire the fix(chat): ack-drop resource wakes missing Slack thread #1721 ack-dropbranch with it.
Delivery separate.
sessionSourcetoLocation. Remove
turn-session-routing.AgentRun.destination,destinationVisibility, the fake localDestination, and
AgentRun.publishExternally.the final contract.
Outcome
The work is complete when:
Location.
AgentRuncarries that Location once.fake Message/Thread and without requiring
threadTson channel-levelLocations.
slack-resource-event.tsand the JUNIOR-90 ack-drop stopgap are gone becausethe plain path makes them unnecessary.
Run without translating between duplicate names.
Requested by David Cramer.
--
View Junior Session [Sentry]