Skip to content

feat(hub): let Hub send a message to an execution's live agent - #4270

Closed
as-cto wants to merge 1 commit into
getpaseo:mainfrom
as-cto:feat/hub-execution-prompt
Closed

feat(hub): let Hub send a message to an execution's live agent#4270
as-cto wants to merge 1 commit into
getpaseo:mainfrom
as-cto:feat/hub-execution-prompt

Conversation

@as-cto

@as-cto as-cto commented Sep 3, 2026

Copy link
Copy Markdown

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 reuses sendPromptToAgent, so it inherits the dispatch semantics chat and schedules already use, and activeTurnBehavior decides what happens when a turn is in flight — steer folds the message into it, interrupt replaces 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;
  • an execution with no live agent answers delivered: false rather 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

  • Unit tests added for the controller (delivery, no live agent, blank prompt, daemon failure, post-cleanup silence) and for DaemonExecutions.prompt (delivered, archived agent, unknown execution, revoked authority). vitest green on the hub suites; typecheck, lint and format clean on the changed files.
  • Running in production on three of our daemons (0.7.2 + this commit) since today, driven by a Hub build that uses it: three consecutive messages in one Linear session served by the same agent, which then restated the earlier requests from memory rather than from a replayed transcript.
  • Platforms tested: Linux (Ubuntu 24.04, Node 24). Not tested: macOS, Windows.

Compatibility

Additive. A Hub that sends it to a daemon predating the message gets rpc_error and 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.

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>
@as-cto
as-cto marked this pull request as ready for review September 3, 2026 23:28
@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a correlated Hub RPC for sending prompts to an execution’s existing agent, including permission mapping, session dispatch, per-execution ordering, lifecycle wiring, and controller/daemon tests.

  • Adds prompt request and response schemas to the shared protocol.
  • Routes authorized Hub prompts through the execution controller to sendPromptToAgent without unarchiving archived agents.
  • Serializes prompts with execution control actions and reports delivery disposition or fallback eligibility.
  • Adds focused tests for validation, delivery, archived and unknown executions, failures, authority revocation, and cleanup.

Confidence Score: 4/5

The teardown race should be fixed before merging because a Hub prompt can continue executing after the relationship authority has been retired.

Authority invalidation omits prompt operations from the work it awaits, allowing accepted agent work to outlive stop or disconnect; completed prompt tails also remain retained for the relationship lifetime.

Files Needing Attention: packages/server/src/server/hub/daemon-executions.ts

Important Files Changed

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
Loading

Comments Outside Diff (1)

  1. packages/server/src/server/hub/daemon-executions.ts, line 202-205 (link)

    P1 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

Comment on lines +157 to +163
this.controlTails.set(
executionKey,
prompt.then(
() => undefined,
() => undefined,
),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@boudra

boudra commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Closing in favor of the implementation merged in #4354, which lets Hub continue agents through the standard daemon API.

@boudra boudra closed this Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants