Skip to content

feat(signals): run agent tasks in parallel with session and resource locks - #34

Open
jannikluhn wants to merge 5 commits into
mainfrom
feat/parallelization
Open

jannikluhn wants to merge 5 commits into
mainfrom
feat/parallelization

Conversation

@jannikluhn

@jannikluhn jannikluhn commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What this does

The signal worker currently runs one agent task at a time, so one user's task can hold up everyone else. This change lets runs execute in parallel when they use different sessions and resources. Runs that share either still execute in order.

Each handler can list the resources a prompt needs exclusive access to, such as a workspace, an account, or a user's budget. The worker starts a run once no earlier unfinished run uses the same session or any of those resources.

claim signals and call their handlers one at a time, in emission order
  handle(signal) → prompts
  queue the prompts in order
  start each run when no earlier unfinished run shares
    its session or any of its resources
  give each run its own connection to the agent instance
  after all runs for a signal finish
    call post(signal, { failed, limited }), if defined
    release the sessions and resources after post returns

Without any resource declarations, runs in different sessions can run in parallel automatically. Resource names act as exclusive locks: any two runs naming the same resource must wait for each other. There is no configurable concurrency limit.

What a handler gains

 type Prompt = {
   session: string | null
   text: string
   usageLimit?: number
   context?: unknown
+  resources?: readonly string[]   // resources this run needs exclusive access to
+  tag?: string                    // random run tag, also included in the prompt text
 }

Matching an agent server request to its run

With several runs active, the agent server needs a way to tell which run sent a request. The handler generates a random tag, includes it in the prompt text, and tells the agent to send it in the concorde-run-tag header. The worker looks up that tag and sets request.concordeRun to the matching run's metadata.

sequenceDiagram
    participant H as Signal handler
    participant W as Signal worker
    participant A as Agent instance
    participant S as Agent server
    H->>W: Prompt containing the tag in text and tag fields
    W->>A: Start run and register its tag
    A->>S: Request with concorde-run-tag header
    S->>W: Look up tag
    W-->>S: request.concordeRun = { id, signalId, session, context }
    Note over S: Missing, unknown, or expired tag leaves concordeRun undefined
    W->>W: Remove tag when the run ends
Loading

The server never guesses based on which runs are active, even if there is only one. Tags help prevent injected message text from impersonating another run, but they do not provide access control. Runs share the agent instance's filesystem, where another session's tag can be read. This limitation is documented in the architecture guide.

Commits

  • Match agent server requests to runs using their tags.
  • Allow runs in different sessions to execute in parallel.
  • Add optional resource locks to prompts.
  • Test request attribution while several runs are active.
  • Keep sessions and resources locked until the signal's post callback finishes.

The runtime interface stays the same. Reported validation: npm run check passes. Gateway tests check request.concordeRun through actual HTTP requests; worker tests use a fake runtime with controlled pauses to check ordering and parallel execution.

jannikluhn and others added 5 commits November 5, 2026 21:37
… it carries

A Signal Handler mints a random tag per Prompt, writes it into the Prompt text and sets it as
tag on the Prompt. The agent echoes it as the concorde-run-tag header; the Worker holds live
tags only while their Runs are in flight, and request.concordeRun names the Run whose tag the
request carries or nothing, with no fallback to the Run in flight. A request with no header,
an unknown tag or a dead tag is attributed to no Run. A Prompt whose tag is already live on
another Run fails instead of taking that Run's identity. The template Handler takes a tag
function and exposes the value to the template. The isolation example mints the tag and
enforces its per-Run rule. The Runtime never sees the tag and nothing is stored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A queued Run starts when no earlier unfinished Run, running or waiting, names its Session;
the queue is rescanned from the head on every Run end, so order among conflicting Runs is
emission order. Signals are still claimed and handled one at a time, the next one only when
every queued Run has started. A Signal's post phase runs once its last Run ends, Signals
settle in whatever order their Runs finish, stop lets every claimed Run reach its outcome,
and recovery after a restart is unchanged. Each Run's tag stays its own while Runs overlap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A Signal Handler writes resources, an optional read-only array of strings, on a Prompt; two
Runs sharing any Resource never overlap and keep their emission order. The Session is never
in the array and is held separately. The template Handler takes a resources function of the
Signal. The Runtime never sees the field and nothing is stored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two Users' Runs in flight at once each resolve only to their own tag, at the Worker seam
through the probe route and at the Gateway seam through the cross-User hook, and a request
from one Run carrying the other Run's tag is attributed to the other Run, which is the
documented limit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s post phase

A Run whose Handler has a post phase stays queued after it ends, until that phase returns, so
a later Signal's Run in the same Session or sharing a Resource waits for the post phase too,
which may still be using what the Run held. A Run whose Handler has no post phase releases at
its end as before, and an ended Run never blocks a Run of its own Signal, which the post phase
is waiting for. The Handler doc now also states that handle of a later Signal runs while
earlier Signals' Runs and post phases are in flight.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jannikluhn jannikluhn changed the title feat(signals): run Runs in parallel, serialized by Session and Resource feat(signals): run agent tasks in parallel with session and resource locks Nov 9, 2026

This branch has not been deployed

No deployments
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.

1 participant