Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
168 commits
Select commit Hold shift + click to select a range
7261238
feat(distributed): store the active carrier in a cluster row
mudler Oct 7, 2026
14e922c
refactor(distributed): take NodeControl where a concrete sender was used
mudler Oct 7, 2026
d441859
feat(distributed): add forwarding holders for the carrier seams
mudler Oct 7, 2026
7ad74d3
refactor(distributed): build the distributed services over carrier ho…
mudler Oct 7, 2026
c6b7b33
ci: run the distributed e2e suite as an advisory job
mudler Oct 7, 2026
859f198
docs(agents): describe the carrier holders and the cluster row
mudler Oct 7, 2026
09ea080
test(distributed): run the broadcaster conformance suite on the holder
mudler Oct 7, 2026
ff195e3
Merge remote-tracking branch 'origin/master' into feat/live-carrier-b…
mudler Oct 7, 2026
59c0311
feat(distributed): add the cluster registry of replicas and worker co…
mudler Oct 7, 2026
3e04ea1
feat(distributed): record which carriers a worker node is attached to
mudler Oct 7, 2026
b8dce7b
feat(distributed): register every frontend replica in the cluster reg…
mudler Oct 7, 2026
ad2ff0f
refactor(messaging): share the subject matcher and bound broadcast pa…
mudler Oct 7, 2026
c3c6af2
test(testutil): keep the database in memory and return its connection…
mudler Oct 7, 2026
05ff501
feat(messaging): refuse the control roots on a carrier with no reques…
mudler Oct 7, 2026
cccf81b
feat(pgbus): add a broadcast carrier over PostgreSQL LISTEN and NOTIFY
mudler Oct 7, 2026
029e5a0
fix(pgbus): read spilled broadcasts with several readers and keep the…
mudler Oct 7, 2026
ce13f63
feat(carrier): build the pgbus fan-out as a member of a carrier set
mudler Oct 7, 2026
ae780e7
feat(tunnel): add the wire format and the session of the worker tunnel
mudler Oct 7, 2026
9bc6245
feat(tunnel): pass the end of a stream on when two streams are joined
mudler Oct 7, 2026
c5b00ab
test(tunnel): measure the delay of a small call during a large transfer
mudler Oct 7, 2026
a27b911
feat(tunnel): hold the tunnels of a replica and tie each to a claim
mudler Oct 7, 2026
48b50b2
feat(cluster): claim the held connections again after a sweep
mudler Oct 7, 2026
2088ea3
feat(nodes): mint a tunnel credential for every node at registration
mudler Oct 7, 2026
5ea6a1f
feat(http): serve the connect route of the worker tunnel
mudler Oct 7, 2026
b27be57
test(tunnel): move the slow link of the latency spec to a package
mudler Oct 7, 2026
8f9d086
feat(nodes): add the readiness probe of a worker on the tunnel
mudler Oct 7, 2026
67cf45c
feat(worker): keep the credentials of the carrier that registration n…
mudler Oct 7, 2026
c037092
feat(worker): add the client of the worker tunnel
mudler Oct 7, 2026
06f1041
test(tunnel): require the shared constructors for every websocket
mudler Oct 7, 2026
73934c1
feat(worker): attach to the carrier that the frontend names
mudler Oct 7, 2026
42d4b2c
feat(cli): let the agent worker learn the carrier from the registration
mudler Oct 7, 2026
bb5b5a9
docs(agents): describe the worker tunnel for contributors
mudler Oct 7, 2026
d706394
fix(tunnel): close joined streams when the session ends under a half-…
mudler Oct 7, 2026
989b73f
refactor(worker): drop an accessor of the credential manager that not…
mudler Oct 7, 2026
fdcc25f
refactor(worker): remove the single-attempt registration that retry n…
mudler Oct 7, 2026
9911052
fix(pgbus): bound the connections that publishing uses
mudler Oct 7, 2026
08d2dab
fix(pgbus): drop a spilled broadcast whose read does not end
mudler Oct 7, 2026
d891449
fix(cluster): bound the database calls of the membership loop
mudler Oct 7, 2026
d5f58a1
fix(carrier): refuse a subscription to a control root in the holder
mudler Oct 7, 2026
acc225a
docs(distributed): state the 8 MiB limit of a broadcast
mudler Oct 7, 2026
1c92558
test(cluster): fail on an ordered comparison of an epoch
mudler Oct 7, 2026
c9c6edf
refactor(cluster): drop Registry.Live, which only specs called
mudler Oct 7, 2026
7546943
test(tunnel): cover the claim again after a reaped replica registers
mudler Oct 7, 2026
1634a7a
Merge remote-tracking branch 'origin/master' into feat/live-carrier-b…
mudler Oct 7, 2026
c58d6d4
fix(worker): register again when the frontend refuses the tunnel cred…
mudler Oct 7, 2026
9952340
fix(worker): read only an unreachable target as a reason to reap
mudler Oct 7, 2026
1915690
fix(tunnel): reset the streams that a worker opens on the frontend se…
mudler Oct 8, 2026
e63f86a
test(tunnel): run the latency benchmark only when its label is asked for
mudler Oct 8, 2026
f38b266
feat(tunnel): end held tunnels on demand and let a bulk open refuse t…
mudler Oct 8, 2026
b5adc6f
fix(cluster): end the tunnel of a node that was removed or got a new …
mudler Oct 8, 2026
27d50a3
fix(tunnel): bound the claim of a tunnel and release it when the call…
mudler Oct 8, 2026
3348aa6
fix(worker): warn when the tunnel dials another host over plain ws
mudler Oct 8, 2026
ffb6e99
test(worker): run the latency benchmark of the tunnel client only whe…
mudler Oct 8, 2026
7929d0c
fix(worker): cap the quick redial of the bulk lane after a run of 409
mudler Oct 8, 2026
b3f3d76
feat(workerctl): name the HTTP path of each control verb
mudler Oct 8, 2026
8e34dd4
feat(nodes): mount the control plane of a worker on its file transfer…
mudler Oct 8, 2026
1fe59fe
feat(worker): serve the control verbs over HTTP for a worker on the t…
mudler Oct 8, 2026
009b5ad
feat(nodes): send the control verbs over the HTTP control plane of a …
mudler Oct 8, 2026
bf045cf
feat(cluster): publish the address and the peer credential of a replica
mudler Oct 8, 2026
2b63ab5
feat(tunnel): dial a worker through the replica that holds its tunnel
mudler Oct 8, 2026
e33662d
feat(http): serve the link that replicas dial to reach a tunnel
mudler Oct 8, 2026
67a417a
fix(tunnel): report a bulk lane that is down as that through a relay
mudler Oct 8, 2026
d94aa41
feat(grpc): let a client reach its backend through a dialer and say w…
mudler Oct 8, 2026
c5fd85b
fix(nodes): do not read a failed transport as a dead backend
mudler Oct 8, 2026
12d40e2
feat(nodes): reach the backends and the HTTP server of a worker throu…
mudler Oct 8, 2026
6d788fb
feat(carrier): build the set of seams of the tunnel carrier
mudler Oct 8, 2026
655e187
docs(agents): describe the HTTP control plane, the dial seam and the …
mudler Oct 8, 2026
20c48df
feat(application): forget a node in the active carrier when it loses …
mudler Oct 8, 2026
089f9d9
test(nodes): hold the carriers of the load verbs to two more conditions
mudler Oct 8, 2026
bec673d
docs(distributed): add the flag for the address that peers dial
mudler Oct 8, 2026
4944568
test(carrier): compare the probe on two lanes instead of timing it ag…
mudler Oct 8, 2026
d8dc796
test(nodes): wait for the shorter hold after a stop is acknowledged
mudler Oct 8, 2026
43bd40b
test(http): read the transport of the logs proxy instead of the envir…
mudler Oct 8, 2026
be68af8
feat(workerctl): name the verbs of an agent worker and let a progress…
mudler Oct 8, 2026
a1408e8
feat(nodes): publish the broadcasts that a worker asks for, within an…
mudler Oct 8, 2026
94bb85c
feat(nodes): send the MCP requests of the frontend to an agent worker…
mudler Oct 8, 2026
d4dc46c
feat(agents): bind the events of an agent run to the publisher of its…
mudler Oct 8, 2026
1a77c92
feat(agentworker): serve the control plane of an agent worker over it…
mudler Oct 8, 2026
294b084
feat(cli): let the agent worker attach to the tunnel carrier
mudler Oct 8, 2026
48f9a99
test(messaging): add one conformance suite for the carriers of the wo…
mudler Oct 8, 2026
e97729b
feat(jobs): add the claim table, the queue of the carrier that has no…
mudler Oct 8, 2026
95b07ee
feat(jobs): add the claim queue as a WorkQueue and a consumer for fro…
mudler Oct 8, 2026
d1e50b1
refactor(nodes): let the claim loop pick an agent worker and tell a b…
mudler Oct 8, 2026
81d60ba
feat(jobs): drive claims on agent workers and end a run when its job …
mudler Oct 8, 2026
5a7473a
feat(nodes): demote a node whose tunnel is gone while the tunnel is t…
mudler Oct 8, 2026
5d8dd9f
feat(nodes): stage files through shared object storage on either carrier
mudler Oct 8, 2026
3a01003
test(carrier): run agent workers and the claim queue through the tunn…
mudler Oct 8, 2026
00e222b
docs(distributed): describe the claim queue, the agent worker on the …
mudler Oct 8, 2026
36ebc33
docs(messaging): say what the work queue delivers on each carrier
mudler Oct 8, 2026
c9dc8ce
style: check the errors that the linter asks about
mudler Oct 8, 2026
8a602a5
fix(tunnel): bound the relay frame by the budget of the caller
mudler Oct 8, 2026
b5a4cad
fix(tunnel): let a caller leave the queue for a peer dial when its bu…
mudler Oct 8, 2026
915be99
fix(grpc): report a dial that has not finished as a failure of the tr…
mudler Oct 8, 2026
4be4e45
fix(tunnel): keep a slow or broken path apart from a missing route
mudler Oct 8, 2026
b57c355
fix(tunnel): look the owner up again when the owner no longer holds t…
mudler Oct 8, 2026
372bca0
fix(nodes): serve the control verbs of a worker only to a caller that…
mudler Oct 8, 2026
6912c5e
feat(tunnel): let the peer link run over TLS and warn when it does not
mudler Oct 8, 2026
9871365
fix(nodes): treat a stream that broke during an install as an install…
mudler Oct 8, 2026
9b140d4
fix(nodes): probe a replica again before loading its model a second time
mudler Oct 8, 2026
1e9fd39
fix(worker): keep a control verb running when its caller goes away
mudler Oct 8, 2026
27bc116
fix(carrier): give back the idle streams of the tunnel carrier when i…
mudler Oct 8, 2026
f212740
refactor(nodes): remove StopNode, which nothing calls
mudler Oct 8, 2026
0dcd860
test(worker): split the process kill helper so that the tests build o…
mudler Oct 8, 2026
411d573
test(carrier): judge the probe on a busy lane against the idle lane
mudler Oct 8, 2026
d667b1b
feat(worker): let a worker on NATS attach to a tunnel while it runs
mudler Oct 8, 2026
df2c266
Merge branch 'master' into feat/live-carrier-b1-holders
mudler Oct 8, 2026
13a6f30
fix(application): resync model configs over the broadcaster holder
mudler Oct 8, 2026
45b9b82
feat(cluster): store the settings, the live replicas and the timings …
mudler Oct 8, 2026
5b388be
feat(cluster): add the protocol of a change of carrier
mudler Oct 8, 2026
6270d62
feat(cluster): tell a hook about every move of the carrier row
mudler Oct 8, 2026
5f04d5a
feat(carrier): swap the set of a replica behind the holders, and drai…
mudler Oct 8, 2026
227e0cd
feat(nodes): read the carriers of the workers, and keep a probe from …
mudler Oct 8, 2026
a333b4a
feat(jobs): complete the claim of a run that the end of a drain cut off
mudler Oct 8, 2026
519cd95
feat(application): start on the carrier the cluster names, and follow…
mudler Oct 8, 2026
fcd578c
feat(http): add the admin API of the carrier
mudler Oct 8, 2026
7495839
test(carrier): change the carrier of a cluster with real sockets, a r…
mudler Oct 8, 2026
514963f
docs(agents): describe the change of carrier in the guide to the seams
mudler Oct 8, 2026
5fb5527
test(carrier): fix the findings of the linter in the specs of the swap
mudler Oct 8, 2026
21d922e
fix(carrier): mark the jitter of the poll as not security relevant
mudler Oct 8, 2026
1b3ba02
test(carrier): check the metrics of the swap and the status of the sw…
mudler Oct 8, 2026
ffb2071
fix(carrier): forward UnloadRemoteModel to the method of the same name
mudler Oct 8, 2026
fce258c
Merge remote-tracking branch 'origin/master' into feat/live-carrier-b…
mudler Oct 8, 2026
e631d49
fix(messaging): store the payload size of the work queue spec as 64 bits
mudler Oct 8, 2026
bbec703
feat(messaging): accept the NATS CA as PEM in memory
mudler Oct 8, 2026
b32b4c3
feat(http): hand a worker what it needs to follow a change of carrier
mudler Oct 8, 2026
a3755e4
refactor(nodes): read the carriers of a worker from its report only
mudler Oct 8, 2026
4c28ab3
feat(workerregistry): ask for the credential of one carrier and read …
mudler Oct 8, 2026
468ffdf
feat(worker): follow the carrier that the cluster names
mudler Oct 8, 2026
8269640
feat(agentworker): follow the carrier that the cluster names
mudler Oct 8, 2026
277fcc9
test(carrier): run the follower of a real worker through the changes …
mudler Oct 8, 2026
e287113
feat(nodes): demote a worker that is not on the active carrier once t…
mudler Oct 8, 2026
34032e7
docs(agents): describe how workers follow the carrier
mudler Oct 8, 2026
e832c8d
fix(agentworker): tell the job registration which carrier it serves
mudler Oct 8, 2026
36574fb
fix(application): close the body of a heartbeat answer and mark the C…
mudler Oct 8, 2026
2ea7ae3
test(worker): check what stops a backend worker from following to NATS
mudler Oct 8, 2026
2856e8f
Merge remote-tracking branch 'origin/master' into feat/live-carrier-b…
mudler Oct 8, 2026
5fd1751
fix(pgbus): offer a notification that arrives as the poll window ends
mudler Oct 8, 2026
b9f038c
style(pgbus): format the poll spec
mudler Oct 8, 2026
c8489d4
fix(cluster): make the NATS address of a change one value, and keep c…
mudler Oct 8, 2026
289c82d
fix(jobs): stop a handler whose claim is gone, free the claims of a r…
mudler Oct 8, 2026
13d48c1
fix(jobs): do not run a job again once it started, and honour a cance…
mudler Oct 8, 2026
70c58ac
fix(carrier): put queued work back when the new carrier refuses it, a…
mudler Oct 8, 2026
6e4c9cb
fix(nodes): bind a worker's broadcasts to its run, and require a cont…
mudler Oct 8, 2026
571a7a2
fix(carrier): bound the stop and the hand-over of a swap, close the w…
mudler Oct 8, 2026
2367992
refactor(carrier): remove Fanout.Ready, which nothing reads
mudler Oct 8, 2026
7e3cae2
fix(cluster): refuse a wait that is zero, negative or too short, befo…
mudler Oct 8, 2026
de1d1c5
fix(cluster): keep the note of a change when its drain ends
mudler Oct 8, 2026
7c3640e
fix(nodes): read the age of a worker heartbeat on the clock of the da…
mudler Oct 8, 2026
e514139
fix(nodes): convert the replica index to int32 with a bound
mudler Oct 8, 2026
7c5f486
fix(cluster): count a replica's report only if it came after the chec…
mudler Oct 8, 2026
89150ac
fix(carrier): keep a released set open for the settle time before it …
mudler Oct 8, 2026
5a5522d
feat(mcp): add a read-only get_cluster_carrier tool
mudler Oct 8, 2026
92c9e51
docs(api): document the cluster carrier and settings endpoints in swa…
mudler Oct 8, 2026
81f0946
feat(cli): add local-ai cluster to read and change the carrier
mudler Oct 8, 2026
81c1a81
feat(ui): add a cluster transport panel to the Nodes page
mudler Oct 8, 2026
dc445f5
docs(distributed): describe the two carriers and the live switch
mudler Oct 8, 2026
cfdd3e8
fix(cli): say that NATS is optional in the --distributed help
mudler Oct 8, 2026
8f8b541
test(e2e): change the carrier of a cluster of real processes
mudler Oct 8, 2026
a9ff0aa
ci: run the carrier switch e2e in the distributed workflow
mudler Oct 8, 2026
343f351
Merge branch 'master' into feat/live-carrier-b1-holders
mudler Oct 8, 2026
cf199b9
fix(carrier): run a reconnect hook once for a set that is attached again
mudler Oct 8, 2026
b1424b1
fix(carrier): report ready again after the work of a carrier starts late
mudler Oct 8, 2026
88ea4a3
fix(ui): show the true follow status of a worker, and drop the NATS f…
mudler Oct 8, 2026
e8b94e6
docs(distributed): move the active carrier label off the worker boxes
mudler Oct 8, 2026
8c32a7f
fix(nodes): do not route to a worker that cannot be reached on the ac…
mudler Oct 8, 2026
b8393d3
fix(cluster): do not show an old report of a replica as a blocker whe…
mudler Oct 8, 2026
e34a78d
test(e2e): assert the outcome of a change that a frontend or a worker…
mudler Oct 8, 2026
fcd66b7
test(tunnel): accept a stream that the session reset before the first…
mudler Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
595 changes: 534 additions & 61 deletions .agents/distributed-seams.md

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions .github/workflows/tests-e2e-distributed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: 'E2E Distributed Tests'

on:
pull_request:
# The suite's dependency graph is large, so an allowlist of paths would
# silently stop guarding the moment code moves. It is cheap enough to run
# unless the diff is confined to paths it provably cannot reach.
# See .agents/ci-caching.md.
paths-ignore:
- 'gallery/**'
- 'docs/**'
- 'examples/**'
- '**/*.md'
push:
branches:
- master

concurrency:
group: ci-tests-e2e-distributed-${{ github.event.pull_request.number || github.sha }}-${{ github.repository }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
tests-e2e-distributed:
runs-on: ubuntu-latest
# Advisory: a failure is a visible red X on the pull request, and the job is
# deliberately not in branch protection. Promoting it to a required check
# is a repository-settings change, to be made once it has a track record.
continue-on-error: true
timeout-minutes: 45
steps:
- name: Clone
uses: actions/checkout@v7
with:
submodules: true
- name: Configure apt mirror on runner
uses: ./.github/actions/configure-apt-mirror
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.26.x'
cache: false
- name: Display Go version
run: go version
- name: Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libopus-dev
- name: Proto Dependencies
run: |
curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v26.1/protoc-26.1-linux-x86_64.zip -o protoc.zip && \
unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
rm protoc.zip
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
PATH="$PATH:$HOME/go/bin" make protogen-go
- name: Pre-pull test images
# The suite starts its own PostgreSQL and NATS servers with
# testcontainers. Pulling the images here keeps the pull time out of the
# spec timeouts and makes a registry outage read as a setup failure
# instead of a test failure.
run: |
docker pull postgres:16-alpine
docker pull nats:2-alpine
- name: Distributed E2E
# TESTCONTAINERS_RYUK_DISABLED keeps the pre-pull above meaningful. The
# reaper exists to clean up leaked containers on a long-lived host, but
# this runner is ephemeral and every container dies with the VM.
# Leaving it enabled would pull one more unpinned image mid-suite.
env:
TESTCONTAINERS_RYUK_DISABLED: "true"
run: |
PATH="$PATH:$HOME/go/bin" make test-e2e-distributed
- name: Distributed E2E, switch of the carrier
# Real processes: two frontends and two workers change the carrier
# between NATS and the database tunnel while a model serves requests.
# It runs apart from the suite above so that a failure names the
# switch and the two logs stay short. Like the suite, it needs only the
# public PostgreSQL and NATS images and no secret, so a pull request
# from a fork runs it the same way.
if: ${{ !cancelled() }}
env:
TESTCONTAINERS_RYUK_DISABLED: "true"
run: |
PATH="$PATH:$HOME/go/bin" make test-e2e-distributed-switch
# tmate keeps the runner busy until the 6 hour job limit, so a single
# failure costs a whole runner slot. Only open a session when someone
# asked for one by labelling the pull request `ci-debug`, and cap the
# session so a forgotten label cannot idle a runner either.
- name: Setup tmate session if tests fail
if: ${{ failure() && contains(github.event.pull_request.labels.*.name, 'ci-debug') }}
timeout-minutes: 30
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
connect-timeout-seconds: 180
limit-access-to-actor: true
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,18 @@ test-e2e-distributed: protogen-go
@echo 'Running distributed e2e tests (label Distributed, incl. NatsJWT)'
$(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter='Distributed && !VLLMMultinode' --flake-attempts $(TEST_FLAKES) -v -r ./tests/e2e/distributed

# Carrier switch with real processes. Two frontends and two workers (one
# dual-capable, one tunnel-only) run as child processes against PostgreSQL and
# NATS containers, and the carrier is changed from NATS to the tunnel and back.
# The spec carries the CarrierSwitch label and not Distributed, so
# test-e2e-distributed above does not run it: it starts many processes and takes
# a few minutes. It builds local-ai when LOCALAI_E2E_BIN is not set, and skips
# when docker is missing.
test-e2e-distributed-switch: protogen-go build-mock-backend
@echo 'Running the carrier switch e2e (label CarrierSwitch)'
LOCALAI_E2E_MOCK_BACKEND=$(abspath tests/e2e/mock-backend/mock-backend) \
$(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter='CarrierSwitch' --flake-attempts $(TEST_FLAKES) -v -r ./tests/e2e/distributed

# vLLM multi-node DP smoke (CPU). Builds local-ai:tests and the
# cpu-vllm backend from the current working tree, then drives a
# head + headless follower via testcontainers-go and asserts a chat
Expand Down
2 changes: 1 addition & 1 deletion core/application/agent_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (a *Application) RestartAgentJobService() error {
agentJobService.SetDistributedJobStore(d.JobStore)
}
// Keep agent tasks consistent across replicas (same client the dispatcher uses).
agentJobService.SetTaskSyncNATS(d.Nats)
agentJobService.SetTaskSyncNATS(d.Broadcaster)
}

// Start the service
Expand Down
9 changes: 6 additions & 3 deletions core/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,9 @@ func (a *Application) start() error {
if a.distributed != nil && a.distributed.Unloader != nil {
assistantClient.LoadStopper = a.distributed.Unloader
}
if a.distributed != nil && a.distributed.Switch != nil {
assistantClient.Carrier = a.distributed.Switch
}
// PII filter — same nil-or-real wiring.
assistantClient.PIIRedactor = a.piiRedactor
assistantClient.PIIEvents = a.piiEvents
Expand Down Expand Up @@ -670,8 +673,8 @@ func (a *Application) agentPoolOptions() agentpool.AgentPoolOptions {
opts.WorkQueue = d.WorkQueue
opts.EventBridge = d.AgentBridge
opts.AgentStore = d.AgentStore
if d.Nats != nil {
opts.Bus = d.Nats
if d.Broadcaster != nil {
opts.Bus = d.Broadcaster
}
}
return opts
Expand Down Expand Up @@ -723,7 +726,7 @@ func (a *Application) StartAgentPool() {
}
// Keep per-user agent tasks consistent across replicas (nil in standalone).
if d := a.Distributed(); d != nil {
usm.SetJobSyncNATS(d.Nats)
usm.SetJobSyncNATS(d.Broadcaster)
}
aps.SetUserServicesManager(usm)

Expand Down
Loading
Loading