feat(hub): let Hub send a message to an execution's live agent - #4270
feat(hub): let Hub send a message to an execution's live agent#4270as-cto wants to merge 1 commit into
Conversation
Hub could create an agent, interrupt it, or archive it. Nothing else. A conversational trigger surface — a Linear agent session, where the user keeps typing into the same panel — therefore had no way to reach the agent it had already started: every message forced a new agent and replayed the whole thread as text, which drops the context the previous agent had built and costs a full cold start per turn. `hub.execution.agent.prompt.request` carries the message to the running agent instead, and `activeTurnBehavior` decides what happens when a turn is already in flight: `steer` folds the message into it (the user adding a precision mid-work), `interrupt` replaces it. It reuses `sendPromptToAgent`, so it inherits the dispatch semantics the chat and schedules already use. Two deliberate limits keep the new reach narrow: - `unarchive: false` — a Hub prompt continues a conversation, it never revives an agent whose workspace the daemon already tore down; - an execution with no live agent answers `delivered: false` rather than raising, because "start a fresh one" is the caller's ordinary fallback and not an error to report. Prompts are ordered behind the same per-execution tail as control actions: a prompt racing an archive, or two prompts racing each other, would otherwise reach the agent in whichever order the event loop picked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| packages/server/src/server/hub/daemon-executions.ts | Adds owned prompt delivery and serialization, but authority invalidation omits prompt work and completed prompt tails are retained. |
| packages/server/src/server/hub/execution-controller.ts | Validates prompt fields, maps delivery or errors into correlated responses, and suppresses sends after cleanup. |
| packages/protocol/src/messages.ts | Adds additive prompt request and response contracts to the relevant inbound and outbound unions. |
| packages/server/src/server/authorization/operation-permissions.ts | Correctly places both directions of the new operation behind hub.execute. |
| packages/server/src/server/bootstrap.ts | Wires prompt delivery through sendPromptToAgent with unarchive disabled. |
| packages/server/src/server/session.ts | Dispatches the new inbound message to the Hub execution controller. |
Sequence Diagram
sequenceDiagram
participant Hub
participant Session
participant Controller as HubExecutionController
participant Executions as DaemonExecutions
participant Agent as Live agent
Hub->>Session: hub.execution.agent.prompt.request
Session->>Controller: promptAgent(request)
Controller->>Executions: prompt(executionId, prompt, behavior)
Executions->>Executions: enqueue behind execution tail
alt owned, unarchived execution
Executions->>Agent: sendPromptToAgent
Agent-->>Executions: disposition
Executions-->>Controller: "delivered=true"
else no eligible agent
Executions-->>Controller: "delivered=false"
end
Controller-->>Hub: correlated prompt response
Comments Outside Diff (1)
-
packages/server/src/server/hub/daemon-executions.ts, line 202-205 (link)Authority teardown omits prompts
When a Hub relationship stops or disconnects after a prompt passes its authority checks,
invalidateAuthority()does not await that prompt because it tracks only creates and controls. Teardown therefore completes while the agent continues consuming provider resources and potentially mutating the workspace, and session cleanup can suppress the prompt response.Knowledge Base Used: Daemon server and service runtime
Reviews (1): Last reviewed commit: "feat(hub): let Hub send a message to an ..." | Re-trigger Greptile
| this.controlTails.set( | ||
| executionKey, | ||
| prompt.then( | ||
| () => undefined, | ||
| () => undefined, | ||
| ), | ||
| ); |
There was a problem hiding this comment.
Completed prompt tails remain retained
Every prompted execution leaves a settled promise in controlTails, while only control() has a release path. A long-lived relationship that prompts many distinct execution IDs therefore retains one key and associated closure per execution until the entire relationship is retired.
|
Closing in favor of the implementation merged in #4354, which lets Hub continue agents through the standard daemon API. |
The workflow this unblocks
We self-host Hub and drive it from Linear agent sessions. A session panel is one conversation: the user writes, the agent answers, the user writes again in the same panel.
Hub can only create an agent, interrupt it, or archive it. There is no way to reach the agent it already started, so every message in that one panel starts a new agent and replays the thread as text. Everything the previous agent had built — the files it read, what it had already tried, why it answered as it did — is thrown away between two sentences of the same exchange. It also costs a full cold start per message: measured on our deployment, ~20 s per turn against ~6 s once the agent can simply be spoken to.
The change
One message,
hub.execution.agent.prompt.request, carrying{executionId, prompt, activeTurnBehavior}. It reusessendPromptToAgent, so it inherits the dispatch semantics chat and schedules already use, andactiveTurnBehaviordecides what happens when a turn is in flight —steerfolds the message into it,interruptreplaces it.Two deliberate limits keep the new reach narrow:
unarchive: false— a Hub prompt continues a conversation, it never revives an agent whose workspace the daemon already tore down;delivered: falserather than raising, because "start a fresh one" is the caller's ordinary fallback, not an error.Prompts are ordered behind the same per-execution tail as control actions: a prompt racing an archive, or two prompts racing each other, would otherwise reach the agent in whichever order the event loop picked.
QA
DaemonExecutions.prompt(delivered, archived agent, unknown execution, revoked authority).vitestgreen on the hub suites; typecheck, lint and format clean on the changed files.Compatibility
Additive. A Hub that sends it to a daemon predating the message gets
rpc_errorand falls back to creating an agent, which is today's behaviour.Draft because the direction is worth confirming before I polish: happy to reshape the message, the naming, or the ordering guarantees. Maintainer edits enabled.