Compare commits

..

No commits in common. "01326b05ed54eca7402d1fa6ab3766b3187bb774" and "79e1a48d28593d351a566941638b875f5e962377" have entirely different histories.

132 changed files with 3391 additions and 5582 deletions

View file

@ -1,3 +1,3 @@
[alias] [alias]
xtask = "run --package xtask --" xtask = "run --package xtask --"
myelin-chat = "run --package xtask -- myelin-chat" mvp-chat = "run --package xtask -- mvp-chat"

View file

@ -1,12 +1,14 @@
* *
!apps/ !apps/
!apps/myelin/node-image/ !apps/mvp-node/
!apps/myelin/node-image/** !apps/mvp-node/**
!target/ !target/
target/* target/*
!target/release/ !target/release/
target/release/* target/release/*
!target/release/myelin-worker !target/release/mvp-node
!target/release/mvp-worker-node
!target/debug/ !target/debug/
target/debug/* target/debug/*
!target/debug/myelin-worker !target/debug/mvp-node
!target/debug/mvp-worker-node

3
.gitignore vendored
View file

@ -2,7 +2,10 @@
__pycache__ __pycache__
fuzz/artifacts/** fuzz/artifacts/**
corpus corpus
.loop/
.model-cache/ .model-cache/
.deployment-notes/
.omp/
# environment variables for deployment configurations # environment variables for deployment configurations
.config/ .config/

2
Cargo.lock generated
View file

@ -2517,7 +2517,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "myelin" name = "mvp-system"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"blake3", "blake3",

View file

@ -11,37 +11,16 @@ members = [
"crates/datastream", "crates/datastream",
"crates/data-plane", "crates/data-plane",
"crates/dashboard", "crates/dashboard",
"apps/myelin", "crates/mvp-system",
"xtask", "xtask",
"tools/vastai", "tools/vastai",
] ]
default-members = [ exclude = ["crates/bindings/wasm-crypto", "examples", "apps"]
".",
"crates/process",
"crates/provisioning",
"crates/transport",
"crates/distribution",
"crates/iroh-driver",
"crates/datastream",
"crates/data-plane",
"crates/dashboard",
"apps/myelin",
"tools/vastai",
]
# The language bindings (python, wasm) and xtask are built on demand
# (-p / --workspace / cargo-xtask), not during normal native iteration.
exclude = ["crates/bindings/wasm-crypto", "examples"]
[workspace.dependencies]
# Centralized so every member resolves tokio with the SAME feature set.
# Without this, members declared tokio with different features, so building
# one member vs another (or `run` vs `test`) recompiled tokio each time.
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "process", "io-util", "sync", "time", "net", "signal"] }
[package] [package]
name = "swactor" name = "swactor"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "AGPL-3.0-only"
autobenches = false autobenches = false
[profile.bench] [profile.bench]

661
LICENSE
View file

@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

397
README.md
View file

@ -1,392 +1,23 @@
# swactor # swactor
A small, WASM-first actor runtime for Rust: one trait, location-transparent Minimal actor runtime for Rust. One trait, one message type.
addresses, and a built-in cluster. Actors are ordinary structs; a message is Single-threaded (`tick()`) or multi-threaded (`run()`).
any `Clone + Send + Sync` type; an address works the same whether the actor
lives in this process, on a peer, or behind NAT on another node.
## What it is ## Description
Every actor is one trait. There is no separate message trait to implement — Core runtime is `src/`. Actors implement `ActorInterface` (in `actor.rs`),
anything `Clone + Send + Sync` is a message by blanket impl: interact through `Ctx` (in `runtime.rs`), and run on worker threads (`worker.rs`).
```rust `crates/` builds upward: `std` adds OTP patterns (supervision, monitoring, groups),
// src/actor.rs `distribution` adds clustering, everything else composes from there.
pub trait Message: 'static + Sized + Clone + Send + Sync {}
impl<T: 'static + Sized + Clone + Send + Sync> Message for T {} ## Dev commands
`cargo xtask --help` for the basic test command.
## Testing
pub trait ActorInterface: 'static + Send {
type Incoming: Message;
type Response: Message;
fn handle(&mut self, ctx: &Ctx, msg: Self::Incoming);
// ...on_start, on_stop, handle_down (death monitoring) as needed
}
``` ```
You drive the runtime single-threaded with `tick()` (this is what compiles to
WASM) or multi-threaded with `run()`:
```rust
use swactor::actor::{ActorInterface, Ctx};
use swactor::runtime::{Runtime, RuntimeConfig};
struct Counter { count: u64 }
impl ActorInterface for Counter {
type Incoming = (); // `()` is Clone + Send + Sync, so it's a message
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _: Self::Incoming) {
self.count += 1;
println!("hit #{self.count}");
}
}
fn main() -> Result<(), swactor::Error> {
let rt = Runtime::new(RuntimeConfig::default()); // 1 worker → drive with tick()
let addr = rt.spawn(Counter { count: 0 })?;
rt.send_to(addr, ())?;
for _ in 0..3 { rt.tick(); } // spawn → handle → done
Ok(())
}
```
`RuntimeConfig` is three knobs — `max_actors`, `channel_buffer_size`,
and a per-tick `actor_message_budget` (inspired by BEAM's
reduction count, so one chatty mailbox can't starve the rest).
## Features
Each feature is shown in the code that implements it.
### Route messages by actor_id across cluster boundaries
An actor's identity is a 32-byte address, not a pointer:
```rust
// src/actor.rs
pub struct ActorAddress(pub [u8; 32]);
```
You send to it the same way whether it lives in this process, on a peer across
the room, or behind NAT on another continent — one call, `ctx.send(addr, msg)`.
The locality decision is exactly one address-map lookup; a miss hands the
message to the cluster transport instead of a local worker:
```rust
// src/delivery.rs — the cluster boundary
fn route_nonlocal(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
#[cfg(feature = "transport")]
{
if self.inbox_registry.contains(&addr) { // a pending reply inbox
return self.inbox_registry.try_deliver(addr, msg);
}
if let Some(sink) = self.remote_sink { // ← off this node
return sink.send(addr, msg);
}
}
self.inbox_registry.try_deliver(addr, msg)
}
```
The transport seam is a single trait — any cluster driver satisfies it:
```rust
// src/runtime.rs
pub trait RemoteSink: Send + Sync {
fn send(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
}
```
Where `addr` lives is resolved from a signed, gossiped directory: a host-signed
`DirectoryEntry` binds `actor_addr → node_id`, backed by an LRU `LocationCache`.
The node's own mailbox (`node_id → ActorAddress`) is rebound on restart without
changing identity, which is why `NodeId` and `ActorAddress` are deliberately
distinct types.
### WASM by default
The same runtime that runs on a thread pool also compiles to `wasm32` and steps
one tick at a time from a host (browser, Node, an embedding app). Randomness,
timers, and the address map all have a no-op/wasm path behind the `wasm`
feature. The [ping-pong demo](examples/ping-pong) is one runtime compiled to a
`.wasm` cdylib and driven by Node:
```sh
cd examples/ping-pong && ./run.sh
```
### Built-in engine and driver, tasks included
A node ships batteries-included: the actor engine (worker threads), a network
driver, and the pump/fanout tasks that wire them — you don't assemble the glue.
The driver tracks its own established send/recv pumps per QUIC stream and emits
lifecycle events (edge ready, stream fault, pump stopped):
```rust
// crates/iroh-driver/src/driver_pumps.rs
pub enum DriverEventOut {
DriverEdgeReady { edge_id: EdgeId },
StreamFault { edge_id: EdgeId },
PumpStopped { edge_id: EdgeId, ring_id: RingId },
}
```
### `iroh` integration — QUIC, TLS, and NAT traversal
The bundled driver is [iroh](https://iroh.computer): QUIC-based peer transport
with built-in TLS, NAT hole-punching, and relay-server fallback. You hand it a
Tokio engine and it runs accepts, dials, stream I/O, retries, and shutdown on
it; peers are admitted through an optional allow-list:
```rust
// crates/iroh-driver/src/iroh_driver.rs
pub struct IrohDriverConfig {
pub secret_key: Option<SecretKey>, // None → fresh random identity
pub relay_mode: RelayMode, // default: n0 production relays
pub node: DistributedNodeConfig,
pub peer_auth: Option<Arc<Mutex<PeerAllowList>>>,
pub additional_alpns: Vec<Vec<u8>>, // opaque to the driver
}
```
### Built-in SWIM cluster
Membership is a real SWIM implementation — direct + indirect (relay) probes,
suspicion timers, and Lifeguard local-health scaling — not heartbeats. The probe
state machine emits typed actions; the host adapter turns them into sends:
```rust
// crates/distribution/src/swim/probe.rs
pub enum SwimAction {
SendPing { to: NodeId, sequence: u64 },
SendPingReq { relay: NodeId, target: NodeId, sequence: u64 }, // indirect probe
Suspect(NodeId),
DeclareDead(NodeId),
Diag(SwimDiagEvent), // RTT samples — observation only, no protocol effect
}
```
Name bindings, node metadata (relay URL, human name), and the actor→host
directory each run as independent gossip actors, so the cluster goes quiet once
it converges.
### OTP-style standard library
Behind the `std` feature, one extension adds the patterns you reach for:
symbolic naming, death monitoring (watching), and process groups. Install it on
the runtime and actors get `CtxWatching` / `CtxNaming`:
```rust
// src/std/extension.rs
pub struct StdExtension {
name_registry: NameRegistry, // logical names → addresses
watch_registry: WatchRegistry, // exit notifications
group_registry: GroupRegistry, // named groups / pub-sub
}
// runtime.with_extension(Arc::new(StdExtension::new()));
```
### Process manager for external processes
Treat an OS process like an actor. Describe it, spawn it onto the runtime, and
send it commands; its lifecycle (started / exited / signaled) arrives as
messages:
```rust
// crates/process/src/types.rs
pub struct ProcessSpec {
pub command: String,
pub args: Vec<String>,
pub env: HashMap<String, String>,
pub working_dir: Option<std::path::PathBuf>,
pub label: Option<String>,
}
```
There's a YAML-driven pipeline layer on top (`crates/process/src/pipeline.rs`)
for multi-step jobs with progress and failure reporting.
### Zero-copy movement of large byte objects
The data-plane ships large, *typed* byte objects between actors — and across
nodes — without copying. The motivating workload is sharded inference: one GPU's
output activation is the next GPU's input, so it rides a bounded ring leased from
a shared arena. A ring is either local (IPC between two actors on one node) or
the two ends of a QUIC edge:
```text
producer → egress ring ──QUIC──▶ ingress ring → consumer
(arena lease; zero-copy at both ends)
```
What travels on a wire edge is typed, so the receiver knows what arrived:
```rust
// crates/data-plane/src/actor.rs
pub enum EdgeKind {
TokenIn,
Activation, // a hidden-state tensor forwarded to the next shard
TokenOut,
}
// crates/data-plane/src/edge_lifecycle.rs
pub struct ObjectSpec {
pub kind: ObjectKind, // Activation
pub dtype: DType, // F16
pub max_extent_bytes: u64,
}
```
The edge actor owns the lifecycle: lease a byte range, install an ingress or
egress ring, and release it only with a `QuiescenceProof`, so a range is never
recycled under a live reader. The worker consumes whole objects off its ingress
ring as plain bytes:
```rust
// apps/myelin/src/node/worker_node_runtime.rs
match ingress::read_object_record(buffer, object_spec, false)? {
ingress::ObjectRecordRead::Incomplete => Ok(None), // wait for more bytes
ingress::ObjectRecordRead::Complete(record) => { // a full activation arrived
let bytes: Vec<u8> = buffer.drain(..record.total_len).collect();
Ok(Some(IngressRecordBytes { bytes, object_id: record.object_id.0, .. }))
}
}
```
### Custom metrics through the datastream
Telemetry is a deliberately dumb pipe: producers tag bytes with a channel id,
nothing in the middle interprets the payload, and views are read-time
projections. To add a metric stream, define a type and implement one constant —
no schema registry to negotiate with:
```rust
// crates/datastream/src/record.rs
pub trait Record {
const CHANNEL: &'static str;
}
// crates/data-plane/src/arena.rs — the arena publishes its own health this way
impl Record for ArenaSample {
const CHANNEL: &'static str = ARENA_SAMPLE_CHANNEL; // "mvp.arena"
}
```
### Built-in dashboard
A read-only HTML/SSE dashboard renders the frames the datastream already
carries — workers, the actor roster, per-actor dossiers, fleet hardware. It sends
no control signals back, so the whole thing is one Axum router:
```rust
// crates/dashboard/src/server.rs
fn router(state: AppState) -> Router {
Router::new()
.route("/", get(root_page))
.route("/events", get(frame_stream)) // live frames over SSE
.route("/api/frames", get(recent_frames))
.route("/api/views", get(views_json))
.route("/api/view/{*path}", get(view_snapshot))
.route("/view/{*path}", get(view_page))
.with_state(state)
}
```
## Architecture
The workspace builds upward from a tiny core. Each crate is one responsibility:
```mermaid
flowchart TD
app["myelin<br/>node · orchestrator"]
driver["iroh-driver<br/>QUIC network driver"]
dist["distribution<br/>SWIM · registry · directory"]
dash["dashboard<br/>read-only view over datastream"]
proc["process<br/>managed external processes"]
dp["data-plane<br/>zero-copy byte movement · IPC · links"]
ds["datastream<br/>metrics / telemetry pipe"]
trans["transport<br/>codec · identity · crypto"]
core["swactor<br/>core runtime + std (OTP)<br/>ActorInterface · Ctx"]
app --> driver
app --> dist
app --> dp
app --> dash
app --> proc
driver --> dist
driver --> ds
dist --> trans
dist --> ds
dash --> ds
proc --> ds
ds --> trans
trans --> core
dp --> core
ds --> core
proc --> core
```
| Crate | Role |
| --- | --- |
| `swactor` (`src/`) | Core runtime: the actor trait, `Ctx`, scheduler, address map, channels |
| `crates/transport` | Codec registry, node identity, message signing — transport-agnostic |
| `crates/distribution` | Clustering: SWIM membership, naming, metadata + actor directory gossip |
| `crates/iroh-driver` | iroh/QUIC network driver with its pump/fanout tasks |
| `crates/datastream` | Metrics / telemetry pipe — nothing in the middle interprets payloads |
| `crates/data-plane` | Zero-copy movement of large typed byte objects over IPC or a network link |
| `crates/process` | Managed external processes and YAML pipelines |
| `crates/dashboard` | Read-only HTML/SSE dashboard over datastream frames |
| `crates/provisioning` | Cloud node provisioning |
| `crates/bindings/{python,wasm-runtime,wasm-crypto}` | Language / target bindings |
| `apps/myelin` | The node binary that wires the above into a runnable cluster node |
| `tools/vastai` | Vast.ai GPU-marketplace tooling |
## Used for
- **A drop-in replacement for GPU jobs on cloud infrastructure.** `apps/myelin`
ships an orchestrator plus a containerized worker node
(`apps/myelin/node-image`) that provisions, stages models, and runs them.
- **Orchestration for sharded inference.** The node image includes a
[tinygrad](https://github.com/tinygrad/tinygrad) worker
(`apps/myelin/node-image/tinygrad_worker.py`); a vLLM backend is planned.
## Examples
**Working:** [`examples/ping-pong`](examples/ping-pong) — two actors volley on a
single-threaded runtime compiled to WebAssembly, driven tick-by-tick from Node.
Shows spawning, message passing, and death monitoring in one file.
**Planned** (tracked in `.deployment-notes/ROADMAP.md`):
- Two actors talking in-process, over IPC, and across a WAN — in Rust, Python,
and WASM — from the same code.
- Typed chunk streaming across IPC or WAN.
- A non-trivially sharded training job (not easily expressed in Ray).
- A full pipeline-parallel example of a model sharded across N ≥ 3 cheap GPUs.
## Developing
Requires the pinned nightly toolchain (`rust-toolchain.toml`):
```sh
cargo check --workspace cargo check --workspace
cargo xtask test # see `cargo xtask --help` cargo xtask test
``` ```
The language bindings (`crates/bindings/*`) build on demand with `-p` /
`--workspace`, not during normal native iteration.
## Status
swactor is early and under active development. The runtime, clustering, and the
`myelin` node are exercised by the workspace test suite, but APIs are still
moving.
## License
Licensed under the [GNU Affero General Public License v3.0 only]
(./LICENSE) (`AGPL-3.0-only`). The AGPL's network clause (§13) requires anyone
who runs a modified swactor as a network service to offer its source to users.
The copyright holder is not bound by their own license and may relicense future
releases freely; external contributors should expect a CLA requirement before
changes are merged.

15
apps/mvp-node/Dockerfile Normal file
View file

@ -0,0 +1,15 @@
# MVP node CODE image — thin layer over the CUDA/tinygrad base.
# Build from workspace root after compiling the Rust binary:
# cargo build --release -p mvp-system --bin mvp-worker-node
# docker build -f apps/mvp-node/Dockerfile.base -t swactor-mvp-node-base:cuda12.6 .
# docker build -f apps/mvp-node/Dockerfile --build-arg BASE_IMAGE=swactor-mvp-node-base:cuda12.6 -t swactor-mvp-node:latest .
ARG BASE_IMAGE=swactor-mvp-node-base:cuda12.6
ARG MVP_NODE_BIN=target/release/mvp-worker-node
FROM ${BASE_IMAGE}
ARG MVP_NODE_BIN=target/release/mvp-worker-node
COPY ${MVP_NODE_BIN} /usr/local/bin/mvp-node
COPY apps/mvp-node/tinygrad_worker.py /usr/local/share/mvp/tinygrad_worker.py
RUN chmod +x /usr/local/bin/mvp-node /usr/local/share/mvp/tinygrad_worker.py

View file

@ -1,6 +1,6 @@
# Myelin node BASE image — CUDA/Python/tinygrad/sshd/PID-1 foundation. # MVP node BASE image — CUDA/Python/tinygrad/sshd/PID-1 foundation.
# Build from workspace root: # Build from workspace root:
# docker build -f apps/myelin/node-image/Dockerfile.base -t myelin-node-base:cuda12.6 . # docker build -f apps/mvp-node/Dockerfile.base -t swactor-mvp-node-base:cuda12.6 .
FROM nvidia/cuda:12.6.3-runtime-ubuntu24.04 AS builder FROM nvidia/cuda:12.6.3-runtime-ubuntu24.04 AS builder
@ -13,17 +13,17 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN python3 -m pip install --no-cache-dir --break-system-packages \ RUN python3 -m pip install --no-cache-dir --break-system-packages \
--target=/opt/myelin-pydeps \ --target=/opt/mvp-pydeps \
tinygrad==0.12.0 numpy && \ tinygrad==0.12.0 numpy && \
find /opt/myelin-pydeps -depth -type d \ find /opt/mvp-pydeps -depth -type d \
\( -name '__pycache__' -o -name 'tests' -o -name 'test' \) \ \( -name '__pycache__' -o -name 'tests' -o -name 'test' \) \
-exec rm -rf {} + && \ -exec rm -rf {} + && \
find /opt/myelin-pydeps -name '*.pyc' -delete && \ find /opt/mvp-pydeps -name '*.pyc' -delete && \
find /opt/myelin-pydeps -type d -name '*.dist-info' -exec rm -rf {} + find /opt/mvp-pydeps -type d -name '*.dist-info' -exec rm -rf {} +
RUN mkdir -p /opt/myelin-nvrtc-include && \ RUN mkdir -p /opt/mvp-nvrtc-include && \
cp -rL /usr/local/cuda/include/. /opt/myelin-nvrtc-include/ && \ cp -rL /usr/local/cuda/include/. /opt/mvp-nvrtc-include/ && \
test -f /opt/myelin-nvrtc-include/vector_types.h test -f /opt/mvp-nvrtc-include/vector_types.h
FROM nvidia/cuda:12.6.3-base-ubuntu24.04 AS runtime FROM nvidia/cuda:12.6.3-base-ubuntu24.04 AS runtime
@ -48,21 +48,21 @@ RUN apt-get update && \
/usr/share/info/* && \ /usr/share/info/* && \
find /usr -depth -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \ find /usr -depth -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
find /usr -type f -name '*.pyc' -delete 2>/dev/null || true && \ find /usr -type f -name '*.pyc' -delete 2>/dev/null || true && \
mkdir -p /usr/local/share/myelin /var/cache/myelin-models /var/log mkdir -p /usr/local/share/mvp /var/cache/mvp-models /var/log
COPY --from=builder /opt/myelin-nvrtc-include/ /usr/local/cuda/include/ COPY --from=builder /opt/mvp-nvrtc-include/ /usr/local/cuda/include/
COPY --from=builder /opt/myelin-pydeps /opt/myelin-pydeps COPY --from=builder /opt/mvp-pydeps /opt/mvp-pydeps
ENV PYTHONPATH=/opt/myelin-pydeps ENV PYTHONPATH=/opt/mvp-pydeps
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV CUDA=1 ENV CUDA=1
ENV DEV=CUDA ENV DEV=CUDA
ENV MYELIN_MODEL_CACHE_DIR=/var/cache/myelin-models ENV MVP_MODEL_CACHE_DIR=/var/cache/mvp-models
ENV MYELIN_TINYGRAD_WORKER=/usr/local/share/myelin/tinygrad_worker.py ENV MVP_TINYGRAD_WORKER=/usr/local/share/mvp/tinygrad_worker.py
COPY apps/myelin/node-image/myelin_entrypoint.sh /usr/local/bin/myelin_entrypoint.sh COPY apps/mvp-node/mvp_entrypoint.sh /usr/local/bin/mvp_entrypoint.sh
RUN chmod +x /usr/local/bin/myelin_entrypoint.sh RUN chmod +x /usr/local/bin/mvp_entrypoint.sh
ARG DEPLOY_PUBKEY="" ARG DEPLOY_PUBKEY=""
RUN if [ -n "$DEPLOY_PUBKEY" ]; then printf '%s\n' "$DEPLOY_PUBKEY" > /etc/myelin_deploy_key.pub; fi RUN if [ -n "$DEPLOY_PUBKEY" ]; then printf '%s\n' "$DEPLOY_PUBKEY" > /etc/mvp_deploy_key.pub; fi
ENTRYPOINT ["/usr/local/bin/myelin_entrypoint.sh"] ENTRYPOINT ["/usr/local/bin/mvp_entrypoint.sh"]

View file

@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_IMAGE=${BASE_IMAGE:-swactor-mvp-node-base:cuda12.6}
IMAGE=${IMAGE:-swactor-mvp-node:latest}
CONTAINER=${CONTAINER:-swactor-mvp-node-e2e-$$}
GPUS=${MVP_CUDA_GPUS:-all}
PROMPT=${MVP_NODE_SELF_TEST_PROMPT:-ping}
TIMEOUT_SECS=${MVP_NODE_E2E_TIMEOUT_SECS:-1800}
FRAME_LOG=${MVP_DATASTREAM_FRAME_LOG:-/var/log/mvp-datastream.ndjson}
cleanup() {
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
}
trap cleanup EXIT
cargo build --release -p mvp-system --bin mvp-worker-node
docker build -f apps/mvp-node/Dockerfile.base -t "$BASE_IMAGE" .
docker build -f apps/mvp-node/Dockerfile --build-arg BASE_IMAGE="$BASE_IMAGE" -t "$IMAGE" .
docker run -d \
--name "$CONTAINER" \
--gpus "$GPUS" \
-e MVP_NODE_SELF_TEST_PROMPT="$PROMPT" \
-e MVP_NODE_MAX_RUNTIME_SECS=1 \
-e MVP_SELF_TEST_MAX_TOKENS="${MVP_SELF_TEST_MAX_TOKENS:-1}" \
-e MVP_MODEL_CACHE_DIR=/var/cache/mvp-models \
-e MVP_DATASTREAM_FRAME_LOG="$FRAME_LOG" \
${HF_TOKEN:+-e HF_TOKEN="$HF_TOKEN"} \
"$IMAGE" >/dev/null
deadline=$((SECONDS + TIMEOUT_SECS))
while (( SECONDS < deadline )); do
logs=$(docker logs "$CONTAINER" 2>&1 || true)
if grep -q '"type":"ready"' <<<"$logs" && grep -q '"type":"self_test_completed"' <<<"$logs"; then
frames=$(docker exec "$CONTAINER" cat "$FRAME_LOG" 2>/dev/null || true)
if grep -q '"channel":"mvp.node.ready"' <<<"$frames" &&
grep -q '"channel":"mvp.worker.weights"' <<<"$frames" &&
grep -q '"channel":"mvp.worker.prompt"' <<<"$frames"; then
printf '%s\n' "$logs"
printf '%s\n' "$frames"
exit 0
fi
fi
if grep -q 'WorkerFatal\|mvp-node: .*failed\|ModelLoadFailed\|GgufDownloadFailed' <<<"$logs"; then
printf '%s\n' "$logs" >&2
exit 1
fi
sleep 5
done
docker logs "$CONTAINER" 2>&1 || true
echo "mvp-node Docker E2E timed out after ${TIMEOUT_SECS}s" >&2
exit 1

34
apps/mvp-node/mvp_entrypoint.sh Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -u
if ! mkdir /run/mvp_entrypoint.lock 2>/dev/null; then
echo "mvp-entrypoint: already running (lock held); parking" >&2
exec sleep infinity
fi
mkdir -p /root/.ssh && chmod 700 /root/.ssh
: > /root/.ssh/authorized_keys
if [ -n "${PUBLIC_KEY:-}" ]; then printf '%s\n' "$PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then printf '%s\n' "$SSH_PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
if [ -f /etc/mvp_deploy_key.pub ]; then cat /etc/mvp_deploy_key.pub >> /root/.ssh/authorized_keys; fi
chmod 600 /root/.ssh/authorized_keys
if [ ! -s /root/.ssh/authorized_keys ]; then
echo "mvp-entrypoint: WARNING no SSH public key found (PUBLIC_KEY/SSH_PUBLIC_KEY unset, no baked key); SSH will reject logins" >&2
fi
mkdir -p /run/sshd /var/log /var/cache/mvp-models
ssh-keygen -A 2>/dev/null || true
/usr/sbin/sshd -e
echo "mvp-entrypoint: sshd up on :22" >&2
NODE_LOG=/var/log/mvp-node.log
echo "mvp-entrypoint: launching mvp-node (log -> $NODE_LOG)" >&2
set -o pipefail
/usr/local/bin/mvp-node "$@" 2>&1 | tee "$NODE_LOG"
code=${PIPESTATUS[0]}
echo "mvp-entrypoint: mvp-node exited with code $code; NOT restarting (node held for postmortem)" >&2
echo "mvp-entrypoint: --- last 80 lines of $NODE_LOG ---" >&2
tail -n 80 "$NODE_LOG" >&2 || true
exec sleep infinity

View file

@ -107,8 +107,8 @@ def start_cpu_line_sampler(
request_id: int | None, request_id: int | None,
model_id: str | None, model_id: str | None,
) -> CpuLineSampler | None: ) -> CpuLineSampler | None:
raw = os.environ.get("MYELIN_CPU_LINE_PROFILE") raw = os.environ.get("MVP_CPU_LINE_PROFILE")
if not env_flag("MYELIN_CPU_LINE_PROFILE", False): if not env_flag("MVP_CPU_LINE_PROFILE", False):
control( control(
type="CpuLineProfileSkipped", type="CpuLineProfileSkipped",
phase=phase, phase=phase,
@ -117,7 +117,7 @@ def start_cpu_line_sampler(
env_value=raw, env_value=raw,
) )
return None return None
interval_ms = float(os.environ.get("MYELIN_CPU_LINE_PROFILE_INTERVAL_MS", "2")) interval_ms = float(os.environ.get("MVP_CPU_LINE_PROFILE_INTERVAL_MS", "2"))
interval_secs = max(0.0005, interval_ms / 1000.0) interval_secs = max(0.0005, interval_ms / 1000.0)
control( control(
type="CpuLineProfileStarted", type="CpuLineProfileStarted",
@ -158,7 +158,7 @@ def benchmark_stamp() -> dict[str, Any]:
"component": "tinygrad-worker", "component": "tinygrad-worker",
"producer_component": "tinygrad-worker", "producer_component": "tinygrad-worker",
"producer_instance_id": os.environ.get( "producer_instance_id": os.environ.get(
"MYELIN_BENCHMARK_PRODUCER_INSTANCE", "MVP_BENCHMARK_PRODUCER_INSTANCE",
f"tinygrad-worker:{pid}", f"tinygrad-worker:{pid}",
), ),
"producer_process_id": pid, "producer_process_id": pid,
@ -190,12 +190,12 @@ def datastream_endpoint_snapshot() -> dict[str, Any]:
return { return {
"role": "python-worker-stdio-json-bridge", "role": "python-worker-stdio-json-bridge",
"transport": "stdout-json-lines", "transport": "stdout-json-lines",
"endpoint_identity": os.environ.get("MYELIN_DATASTREAM_ENDPOINT_ID", "worker-stdio-bridge"), "endpoint_identity": os.environ.get("MVP_DATASTREAM_ENDPOINT_ID", "worker-stdio-bridge"),
"configured_source": "worker-node-env", "configured_source": "worker-node-env",
"resolved_source": "TinygradWorker::spawn environment", "resolved_source": "TinygradWorker::spawn environment",
"authentication_present": False, "authentication_present": False,
"tls_present": False, "tls_present": False,
"relay_mode": os.environ.get("MYELIN_IROH_RELAY_MODE"), "relay_mode": os.environ.get("MVP_IROH_RELAY_MODE"),
"endpoint_addr_mask": os.environ.get("MVP_IROH_ENDPOINT_ADDR_MASK"), "endpoint_addr_mask": os.environ.get("MVP_IROH_ENDPOINT_ADDR_MASK"),
"connectivity_result": "configured", "connectivity_result": "configured",
} }
@ -225,17 +225,17 @@ def apply_canonical_envelope(event: dict[str, Any]) -> None:
def control(**event: Any) -> None: def control(**event: Any) -> None:
apply_canonical_envelope(event) apply_canonical_envelope(event)
if (run_id := env_int("MYELIN_RUN_ID")) is not None: if (run_id := env_int("MVP_RUN_ID")) is not None:
event.setdefault("run_id", run_id) event.setdefault("run_id", run_id)
if (node_id := env_int("MYELIN_LOGICAL_NODE_ID")) is not None: if (node_id := env_int("MVP_LOGICAL_NODE_ID")) is not None:
event.setdefault("node_id", node_id) event.setdefault("node_id", node_id)
if (stage_index := env_int("MYELIN_STAGE_INDEX")) is not None: if (stage_index := env_int("MVP_STAGE_INDEX")) is not None:
event.setdefault("stage_index", stage_index) event.setdefault("stage_index", stage_index)
print(json.dumps(event, separators=(",", ":")), flush=True) print(json.dumps(event, separators=(",", ":")), flush=True)
def log(message: str) -> None: def log(message: str) -> None:
print(f"myelin_tinygrad_worker: {message}", file=sys.stderr, flush=True) print(f"mvp_tinygrad_worker: {message}", file=sys.stderr, flush=True)
def fatal(reason: str, **fields: Any) -> None: def fatal(reason: str, **fields: Any) -> None:
@ -273,9 +273,9 @@ def initialize(cmd: dict[str, Any]) -> None:
fatal("UnsupportedHelperAbi", helper_abi_version=cmd.get("helper_abi_version")) fatal("UnsupportedHelperAbi", helper_abi_version=cmd.get("helper_abi_version"))
requested_device = str(cmd.get("backend", {}).get("device") or os.environ.get("DEV") or "CUDA") requested_device = str(cmd.get("backend", {}).get("device") or os.environ.get("DEV") or "CUDA")
device = select_tinygrad_device(requested_device) device = select_tinygrad_device(requested_device)
arena_fd = os.environ.get("MYELIN_ARENA_FD") arena_fd = os.environ.get("MVP_ARENA_FD")
if arena_fd is not None: if arena_fd is not None:
arena_bytes = int(os.environ.get("MYELIN_ARENA_BYTES", "0") or "0") arena_bytes = int(os.environ.get("MVP_ARENA_BYTES", "0") or "0")
if arena_bytes > 0: if arena_bytes > 0:
arena = mmap.mmap(int(arena_fd), arena_bytes) arena = mmap.mmap(int(arena_fd), arena_bytes)
started = time.monotonic() started = time.monotonic()
@ -311,8 +311,8 @@ def configure_role(cmd: dict[str, Any]) -> None:
def cache_root() -> Path: def cache_root() -> Path:
raw = os.environ.get("MYELIN_MODEL_CACHE_DIR", "").strip() raw = os.environ.get("MVP_MODEL_CACHE_DIR", "").strip()
root = Path(raw).expanduser() if raw else Path.home() / ".cache" / "myelin-node" root = Path(raw).expanduser() if raw else Path.home() / ".cache" / "mvp-node"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
return root return root
@ -351,7 +351,7 @@ def cache_path_for(url: str) -> Path:
def request_headers() -> dict[str, str]: def request_headers() -> dict[str, str]:
headers = {"User-Agent": "myelin-node/0.1"} headers = {"User-Agent": "swactor-mvp-node/0.1"}
token = os.environ.get("HF_TOKEN", "").strip() token = os.environ.get("HF_TOKEN", "").strip()
if token: if token:
headers["Authorization"] = f"Bearer {token}" headers["Authorization"] = f"Bearer {token}"
@ -392,7 +392,7 @@ def fetch_whole(source: dict[str, Any]) -> Path:
out.write(chunk) out.write(chunk)
done += len(chunk) done += len(chunk)
now = time.monotonic() now = time.monotonic()
if now - last_event >= float(os.environ.get("MYELIN_DOWNLOAD_PROGRESS_SECS", "5")): if now - last_event >= float(os.environ.get("MVP_DOWNLOAD_PROGRESS_SECS", "5")):
control( control(
type="GgufDownloadProgress", type="GgufDownloadProgress",
bytes_done=done, bytes_done=done,
@ -672,7 +672,7 @@ def load_weights(cmd: dict[str, Any]) -> None:
llm_backend = "tinygrad.apps.llm" llm_backend = "tinygrad.apps.llm"
control(type="TinygradLlmImportReady", model_id=model_id, backend=llm_backend) control(type="TinygradLlmImportReady", model_id=model_id, backend=llm_backend)
max_context_raw = os.environ.get("MYELIN_MAX_CONTEXT", "512") max_context_raw = os.environ.get("MVP_MAX_CONTEXT", "512")
max_context = int(max_context_raw) if max_context_raw else 512 max_context = int(max_context_raw) if max_context_raw else 512
control( control(
type="PipelineStageFromGgufStarted", type="PipelineStageFromGgufStarted",
@ -744,7 +744,7 @@ def load_weights(cmd: dict[str, Any]) -> None:
def prompt_template_name() -> str: def prompt_template_name() -> str:
explicit = os.environ.get("MYELIN_PROMPT_TEMPLATE") explicit = os.environ.get("MVP_PROMPT_TEMPLATE")
if explicit is not None: if explicit is not None:
return explicit.strip().lower() return explicit.strip().lower()
tokenizer_pre = str(loaded.get("tokenizer_pre", "")).lower() tokenizer_pre = str(loaded.get("tokenizer_pre", "")).lower()
@ -911,7 +911,7 @@ def infer_prompt(cmd: dict[str, Any]) -> None:
prompt_tokens=len(prompt_tokens), prompt_tokens=len(prompt_tokens),
elapsed_ms=int((time.monotonic() - encode_started) * 1000), elapsed_ms=int((time.monotonic() - encode_started) * 1000),
) )
progress_every = int(os.environ.get("MYELIN_TOKEN_PROGRESS_EVERY", "16") or "16") progress_every = int(os.environ.get("MVP_TOKEN_PROGRESS_EVERY", "16") or "16")
decode_started = time.monotonic() decode_started = time.monotonic()
control( control(
type="DecodeStarted", type="DecodeStarted",

View file

@ -1,48 +0,0 @@
[package]
name = "myelin"
version = "0.1.0"
edition = "2024"
license = "AGPL-3.0-only"
publish = false
autobins = false
[features]
default = []
dashboard = []
[dependencies]
datastream = { path = "../../crates/datastream" }
data-plane = { path = "../../crates/data-plane" }
provisioning = { path = "../../crates/provisioning" }
dashboard = { path = "../../crates/dashboard" }
serde_json = "1"
serde = { version = "1", features = ["derive"] }
swactor = { path = "../..", features = ["serde", "transport"] }
swactor-transport = { path = "../../crates/transport" }
swactor-process = { path = "../../crates/process" }
distribution = { path = "../../crates/distribution" }
iroh-driver = { path = "../../crates/iroh-driver" }
iroh = "0.98"
tokio.workspace = true
swactor-vastai = { path = "../../tools/vastai" }
parking_lot = "0.12"
blake3 = "1"
toml = "0.8"
ureq = "2"
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
signal-hook = "0.3"
[[bin]]
name = "myelin-worker"
path = "src/bin/worker_node.rs"
[[bin]]
name = "myelin-orchestrator"
path = "src/bin/orchestrator.rs"
[[bin]]
name = "myelin-chat"
path = "src/bin/chat.rs"

View file

@ -1,15 +0,0 @@
# Myelin node CODE image — thin layer over the CUDA/tinygrad base.
# Build from workspace root after compiling the Rust binary:
# cargo build --release -p myelin --bin myelin-worker
# docker build -f apps/myelin/node-image/Dockerfile.base -t myelin-node-base:cuda12.6 .
# docker build -f apps/myelin/node-image/Dockerfile --build-arg BASE_IMAGE=myelin-node-base:cuda12.6 -t myelin-node:latest .
ARG BASE_IMAGE=myelin-node-base:cuda12.6
ARG MYELIN_NODE_BIN=target/release/myelin-worker
FROM ${BASE_IMAGE}
ARG MYELIN_NODE_BIN=target/release/myelin-worker
COPY ${MYELIN_NODE_BIN} /usr/local/bin/myelin-node
COPY apps/myelin/node-image/tinygrad_worker.py /usr/local/share/myelin/tinygrad_worker.py
RUN chmod +x /usr/local/bin/myelin-node /usr/local/share/myelin/tinygrad_worker.py

View file

@ -1,54 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_IMAGE=${BASE_IMAGE:-myelin-node-base:cuda12.6}
IMAGE=${IMAGE:-myelin-node:latest}
CONTAINER=${CONTAINER:-myelin-node-e2e-$$}
GPUS=${MYELIN_CUDA_GPUS:-all}
PROMPT=${MYELIN_NODE_SELF_TEST_PROMPT:-ping}
TIMEOUT_SECS=${MYELIN_NODE_E2E_TIMEOUT_SECS:-1800}
FRAME_LOG=${MYELIN_DATASTREAM_FRAME_LOG:-/var/log/myelin-datastream.ndjson}
cleanup() {
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
}
trap cleanup EXIT
cargo build --release -p myelin --bin myelin-worker
docker build -f apps/myelin/node-image/Dockerfile.base -t "$BASE_IMAGE" .
docker build -f apps/myelin/node-image/Dockerfile --build-arg BASE_IMAGE="$BASE_IMAGE" -t "$IMAGE" .
docker run -d \
--name "$CONTAINER" \
--gpus "$GPUS" \
-e MYELIN_NODE_SELF_TEST_PROMPT="$PROMPT" \
-e MYELIN_NODE_MAX_RUNTIME_SECS=1 \
-e MYELIN_SELF_TEST_MAX_TOKENS="${MYELIN_SELF_TEST_MAX_TOKENS:-1}" \
-e MYELIN_MODEL_CACHE_DIR=/var/cache/myelin-models \
-e MYELIN_DATASTREAM_FRAME_LOG="$FRAME_LOG" \
${HF_TOKEN:+-e HF_TOKEN="$HF_TOKEN"} \
"$IMAGE" >/dev/null
deadline=$((SECONDS + TIMEOUT_SECS))
while (( SECONDS < deadline )); do
logs=$(docker logs "$CONTAINER" 2>&1 || true)
if grep -q '"type":"ready"' <<<"$logs" && grep -q '"type":"self_test_completed"' <<<"$logs"; then
frames=$(docker exec "$CONTAINER" cat "$FRAME_LOG" 2>/dev/null || true)
if grep -q '"channel":"myelin.node.ready"' <<<"$frames" &&
grep -q '"channel":"myelin.worker.weights"' <<<"$frames" &&
grep -q '"channel":"myelin.worker.prompt"' <<<"$frames"; then
printf '%s\n' "$logs"
printf '%s\n' "$frames"
exit 0
fi
fi
if grep -q 'WorkerFatal\|myelin-node: .*failed\|ModelLoadFailed\|GgufDownloadFailed' <<<"$logs"; then
printf '%s\n' "$logs" >&2
exit 1
fi
sleep 5
done
docker logs "$CONTAINER" 2>&1 || true
echo "myelin-node Docker E2E timed out after ${TIMEOUT_SECS}s" >&2
exit 1

View file

@ -1,34 +0,0 @@
#!/usr/bin/env bash
set -u
if ! mkdir /run/myelin_entrypoint.lock 2>/dev/null; then
echo "myelin-entrypoint: already running (lock held); parking" >&2
exec sleep infinity
fi
mkdir -p /root/.ssh && chmod 700 /root/.ssh
: > /root/.ssh/authorized_keys
if [ -n "${PUBLIC_KEY:-}" ]; then printf '%s\n' "$PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then printf '%s\n' "$SSH_PUBLIC_KEY" >> /root/.ssh/authorized_keys; fi
if [ -f /etc/myelin_deploy_key.pub ]; then cat /etc/myelin_deploy_key.pub >> /root/.ssh/authorized_keys; fi
chmod 600 /root/.ssh/authorized_keys
if [ ! -s /root/.ssh/authorized_keys ]; then
echo "myelin-entrypoint: WARNING no SSH public key found (PUBLIC_KEY/SSH_PUBLIC_KEY unset, no baked key); SSH will reject logins" >&2
fi
mkdir -p /run/sshd /var/log /var/cache/myelin-models
ssh-keygen -A 2>/dev/null || true
/usr/sbin/sshd -e
echo "myelin-entrypoint: sshd up on :22" >&2
NODE_LOG=/var/log/myelin-node.log
echo "myelin-entrypoint: launching myelin-node (log -> $NODE_LOG)" >&2
set -o pipefail
/usr/local/bin/myelin-node "$@" 2>&1 | tee "$NODE_LOG"
code=${PIPESTATUS[0]}
echo "myelin-entrypoint: myelin-node exited with code $code; NOT restarting (node held for postmortem)" >&2
echo "myelin-entrypoint: --- last 80 lines of $NODE_LOG ---" >&2
tail -n 80 "$NODE_LOG" >&2 || true
exec sleep infinity

View file

@ -1,3 +0,0 @@
fn main() -> std::process::ExitCode {
myelin::run_chat_from_args(std::env::args().skip(1))
}

View file

@ -1,4 +0,0 @@
//! Myelin operator chat wrapper public surface.
mod node_image;
pub(super) mod runtime;

View file

@ -1,3 +0,0 @@
//! Myelin prompt protocol public surface.
pub(crate) mod rpc;

View file

@ -1,50 +0,0 @@
//! Test-only constructors and harness type aliases for the `tests` tree.
//!
//! These helpers previously lived in the production modules
//! (`orchestration/run_fsm`, `staging::control`) but serve only test code.
//! Defining them here keeps them compiled under `#[cfg(test)]` only, so
//! production builds stay free of the dead-code warnings they would otherwise
//! trigger. Inherent methods resolve through their type, so existing call sites
//! such as `fsm::RunPlan::test_linear(..)` and
//! `stage::WeightSource::embedded_gguf(..)` work unchanged.
use crate::run_fsm::{NodeId as FsmNodeId, OrchestratorRun, RunId as FsmRunId, RunPlan, StageRef};
use crate::run_plan::{GgufSource, TokenizerSource};
use crate::staging::{DeviceHandle, StageController, WeightSource};
impl RunPlan {
/// Build a linear pipeline plan (stage 0 -> stage 1 -> ... -> last stage).
/// Test-only convenience over the public `RunPlan` fields.
pub(crate) fn test_linear(run_id: FsmRunId, stages: Vec<StageRef>) -> Self {
Self { run_id, stages }
}
/// Node ids in declared stage order.
pub(crate) fn stage_nodes(&self) -> Vec<FsmNodeId> {
self.stages.iter().map(|stage| stage.node_id).collect()
}
}
impl DeviceHandle {
/// A handle on the worker's current (first) generation.
pub(crate) fn new_current(id: u64) -> Self {
Self { generation: 1, id }
}
}
impl WeightSource {
/// Weights carried by an embedded GGUF file with an embedded tokenizer.
pub(crate) fn embedded_gguf(model_id: impl Into<String>, path: impl Into<String>) -> Self {
Self::new(
model_id,
GgufSource::LocalPath(path.into()),
TokenizerSource::EmbeddedGguf,
)
}
}
/// Test alias for the orchestrator run core, retained for readable test prose.
pub(crate) type OrchestratorHarness = OrchestratorRun;
/// Test alias for the stage controller core, retained for readable test prose.
pub(crate) type StageControllerHarness = StageController;

View file

@ -2,7 +2,6 @@
name = "python" name = "python"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "AGPL-3.0-only"
[lib] [lib]
name = "swactor" name = "swactor"

View file

@ -8,7 +8,7 @@ use ::swactor::actor::{
Actor, ActorAddress, ActorInterface, AnyActor, Ctx, Environment, SpawnRequest, Actor, ActorAddress, ActorInterface, AnyActor, Ctx, Environment, SpawnRequest,
}; };
use ::swactor::config::RuntimeConfig; use ::swactor::config::RuntimeConfig;
use ::swactor::runtime::{Inbox, Runtime}; use ::swactor::runtime::{Inbox, Runtime, RuntimeHandle};
// ─── PyMsg newtype ─────────────────────────────────────────────────────────── // ─── PyMsg newtype ───────────────────────────────────────────────────────────
@ -220,6 +220,8 @@ impl PyInbox {
#[pyclass(name = "RuntimeConfig")] #[pyclass(name = "RuntimeConfig")]
#[derive(Clone)] #[derive(Clone)]
pub struct PyRuntimeConfig { pub struct PyRuntimeConfig {
#[pyo3(get, set)]
num_threads: usize,
#[pyo3(get, set)] #[pyo3(get, set)]
max_actors: usize, max_actors: usize,
#[pyo3(get, set)] #[pyo3(get, set)]
@ -231,11 +233,13 @@ impl PyRuntimeConfig {
#[new] #[new]
#[pyo3(signature = ( #[pyo3(signature = (
*, *,
num_threads = 1,
max_actors = 1_000, max_actors = 1_000,
channel_buffer_size = 1_000, channel_buffer_size = 1_000,
))] ))]
fn new(max_actors: usize, channel_buffer_size: usize) -> Self { fn new(num_threads: usize, max_actors: usize, channel_buffer_size: usize) -> Self {
Self { Self {
num_threads,
max_actors, max_actors,
channel_buffer_size, channel_buffer_size,
} }
@ -245,6 +249,7 @@ impl PyRuntimeConfig {
impl From<PyRuntimeConfig> for RuntimeConfig { impl From<PyRuntimeConfig> for RuntimeConfig {
fn from(py: PyRuntimeConfig) -> Self { fn from(py: PyRuntimeConfig) -> Self {
RuntimeConfig { RuntimeConfig {
num_threads: py.num_threads,
max_actors: py.max_actors, max_actors: py.max_actors,
channel_buffer_size: py.channel_buffer_size, channel_buffer_size: py.channel_buffer_size,
..Default::default() ..Default::default()
@ -275,7 +280,7 @@ impl PyRuntime {
fn spawn(&self, handler: PyObject) -> PyResult<PyActorAddress> { fn spawn(&self, handler: PyObject) -> PyResult<PyActorAddress> {
let rt = self.inner.as_ref().ok_or_else(|| { let rt = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available") pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
})?; })?;
let actor = PyActor::new(handler); let actor = PyActor::new(handler);
let addr = rt.spawn(actor).map_err(to_py_err)?; let addr = rt.spawn(actor).map_err(to_py_err)?;
@ -284,14 +289,14 @@ impl PyRuntime {
fn send(&self, addr: &PyActorAddress, msg: PyObject) -> PyResult<()> { fn send(&self, addr: &PyActorAddress, msg: PyObject) -> PyResult<()> {
let rt = self.inner.as_ref().ok_or_else(|| { let rt = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available") pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
})?; })?;
rt.send_to(addr.inner, PyMsg(msg)).map_err(to_py_err) rt.send_to(addr.inner, PyMsg(msg)).map_err(to_py_err)
} }
fn inbox(&self) -> PyResult<PyInbox> { fn inbox(&self) -> PyResult<PyInbox> {
let rt = self.inner.as_ref().ok_or_else(|| { let rt = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available") pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
})?; })?;
let inbox: Inbox<PyMsg> = rt.new_inbox().map_err(to_py_err)?; let inbox: Inbox<PyMsg> = rt.new_inbox().map_err(to_py_err)?;
Ok(PyInbox { inner: inbox }) Ok(PyInbox { inner: inbox })
@ -299,20 +304,97 @@ impl PyRuntime {
fn tick(&self) -> PyResult<()> { fn tick(&self) -> PyResult<()> {
let rt = self.inner.as_ref().ok_or_else(|| { let rt = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available") pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
})?; })?;
rt.tick(); rt.tick();
Ok(()) Ok(())
} }
fn run(&mut self, py: Python<'_>) -> PyResult<PyRuntimeHandle> {
let rt = self.inner.take().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
})?;
let handle = py.allow_threads(|| rt.run().map_err(to_py_err))?;
Ok(PyRuntimeHandle {
inner: Some(handle),
})
}
fn stats(&self) -> PyResult<PyRuntimeStats> { fn stats(&self) -> PyResult<PyRuntimeStats> {
let rt = self.inner.as_ref().ok_or_else(|| { let rt = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("Runtime not available") pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
})?; })?;
Ok(build_stats(rt)) Ok(build_stats(rt))
} }
fn shutdown(&self) -> PyResult<()> {
let rt = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("Runtime consumed by run()")
})?;
rt.shutdown();
Ok(())
}
} }
// ─── PyRuntimeHandle ─────────────────────────────────────────────────────────
#[pyclass(name = "RuntimeHandle")]
pub struct PyRuntimeHandle {
inner: Option<RuntimeHandle>,
}
#[pymethods]
impl PyRuntimeHandle {
fn spawn(&self, handler: PyObject) -> PyResult<PyActorAddress> {
let handle = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
})?;
let actor = PyActor::new(handler);
let addr = handle.runtime.spawn(actor).map_err(to_py_err)?;
Ok(PyActorAddress::from(addr))
}
fn send(&self, addr: &PyActorAddress, msg: PyObject) -> PyResult<()> {
let handle = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
})?;
handle
.runtime
.send_to(addr.inner, PyMsg(msg))
.map_err(to_py_err)
}
fn inbox(&self) -> PyResult<PyInbox> {
let handle = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
})?;
let inbox: Inbox<PyMsg> = handle.runtime.new_inbox().map_err(to_py_err)?;
Ok(PyInbox { inner: inbox })
}
fn stats(&self) -> PyResult<PyRuntimeStats> {
let handle = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
})?;
Ok(build_stats(&handle.runtime))
}
fn shutdown(&self) -> PyResult<()> {
let handle = self.inner.as_ref().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
})?;
handle.shutdown();
Ok(())
}
fn join(&mut self, py: Python<'_>) -> PyResult<()> {
let handle = self.inner.take().ok_or_else(|| {
pyo3::exceptions::PyRuntimeError::new_err("RuntimeHandle consumed by join()")
})?;
py.allow_threads(|| handle.join());
Ok(())
}
}
// ─── ActorInfo / RuntimeStats ──────────────────────────────────────────────── // ─── ActorInfo / RuntimeStats ────────────────────────────────────────────────
@ -436,6 +518,7 @@ fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyInbox>()?; m.add_class::<PyInbox>()?;
m.add_class::<PyRuntimeConfig>()?; m.add_class::<PyRuntimeConfig>()?;
m.add_class::<PyRuntime>()?; m.add_class::<PyRuntime>()?;
m.add_class::<PyRuntimeHandle>()?;
m.add_class::<PyActorInfo>()?; m.add_class::<PyActorInfo>()?;
m.add_class::<PyWorkerInfo>()?; m.add_class::<PyWorkerInfo>()?;
m.add_class::<PyRuntimeStats>()?; m.add_class::<PyRuntimeStats>()?;

View file

@ -2,7 +2,6 @@
name = "wasm-crypto" name = "wasm-crypto"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
license = "AGPL-3.0-only"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]

View file

@ -2,7 +2,6 @@
name = "wasm-runtime" name = "wasm-runtime"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "AGPL-3.0-only"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]

View file

@ -103,7 +103,10 @@ pub struct WasmRuntime {
impl WasmRuntime { impl WasmRuntime {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new() -> Self { pub fn new() -> Self {
let rt = Runtime::new(RuntimeConfig::default()) let rt = Runtime::new(RuntimeConfig {
num_threads: 1,
..RuntimeConfig::default()
})
.with_extension(Arc::new(StdExtension::new())); .with_extension(Arc::new(StdExtension::new()));
Self { rt } Self { rt }
} }

View file

@ -8,4 +8,4 @@ Keep this crate read-only with respect to observed programs.
- It must not send control signals to observed runtimes. - It must not send control signals to observed runtimes.
- It must not require changes outside `crates/dashboard` for dashboard-only work. - It must not require changes outside `crates/dashboard` for dashboard-only work.
Main built-in views: `/view/swactor/workers` (worker-centric) and `/view/swactor/actor-overview` + `/view/swactor/actor-dossier` (actor-centric), all backed by `runtime.stats`, `runtime.workers`, and `runtime.actors` frames when present. Actor views are pure frame consumers and tolerant of publisher shape. Main built-in view: `/view/swactor/workers`, backed by `runtime.stats`, `runtime.workers`, and `runtime.actors` frames when present.

View file

@ -2,7 +2,6 @@
name = "dashboard" name = "dashboard"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "AGPL-3.0-only"
[dependencies] [dependencies]
axum = "0.8" axum = "0.8"
@ -11,5 +10,5 @@ swactor = { path = "../..", features = ["serde"] }
parking_lot = "0.12" parking_lot = "0.12"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tokio.workspace = true tokio = { version = "1", features = ["net", "rt-multi-thread", "sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View file

@ -2,7 +2,7 @@
Read-only HTML/SSE dashboard over incoming datastream frames. Read-only HTML/SSE dashboard over incoming datastream frames.
The crate owns the Axum server, bounded raw frame window, and view registry. Component crates can keep their own view implementations beside their code and register them through `DashboardHandle::register_view`. The built-in swactor views are hosted here because worker/actor/message processing is universal to swactor programs: the worker page, and the actor overview (fused roster) plus per-actor dossier. The crate owns the Axum server, bounded raw frame window, and view registry. Component crates can keep their own view implementations beside their code and register them through `DashboardHandle::register_view`. The built-in swactor worker page is hosted here because worker/actor/message processing is universal to swactor programs.
## Routes ## Routes
@ -16,9 +16,5 @@ The crate owns the Axum server, bounded raw frame window, and view registry. Com
- `GET /api/view/fleet` — fleet and machine telemetry JSON snapshot - `GET /api/view/fleet` — fleet and machine telemetry JSON snapshot
- `GET /view/swactor/workers` — built-in worker page - `GET /view/swactor/workers` — built-in worker page
- `GET /api/view/swactor/workers` — worker page JSON snapshot - `GET /api/view/swactor/workers` — worker page JSON snapshot
- `GET /view/swactor/actor-overview` — built-in actor overview + roster page
- `GET /api/view/swactor/actor-overview` — actor overview JSON snapshot
- `GET /view/swactor/actor-dossier` — built-in per-actor dossier page
- `GET /api/view/swactor/actor-dossier` — actor dossier JSON snapshot
All state is derived from observed frames. The dashboard sends no control signals back to producers. All state is derived from observed frames. The dashboard sends no control signals back to producers.

View file

@ -0,0 +1,412 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use dashboard::swactor::{RUNTIME_ACTORS, RUNTIME_STATS, RUNTIME_WORKERS};
use dashboard::{DashboardConfig, DashboardHandle, FrameEvent, StreamEvent, start_dashboard};
use datastream::frame::{ChannelId, Frame, Lifetime, NodeId, Position, StreamId};
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::{Value, json};
use swactor::actor::{ActorAddress, ActorInterface, Ctx};
use swactor::config::RuntimeConfig;
use swactor::runtime::Runtime;
use swactor::stats::{ActorSnapshot, StatsHook};
const NODE_ID: &str = "dashboard-swactor-dummy";
const WORKER_ACTORS: usize = 12;
const PUBLISH_INTERVAL: Duration = Duration::from_millis(250);
const PULSE_INTERVAL: Duration = Duration::from_millis(25);
const WORK_ITEM_DELAY: Duration = Duration::from_micros(200);
#[derive(Clone)]
struct PulseTick {
seq: u64,
}
#[derive(Clone)]
struct WorkItem {
seq: u64,
route: u32,
hops_left: u8,
}
#[derive(Clone)]
enum RouterMsg {
Configure { workers: Vec<ActorAddress> },
Beat { seq: u64 },
Complete { worker: u32, seq: u64, route: u32 },
}
struct PulseActor {
router: ActorAddress,
}
impl ActorInterface for PulseActor {
type Incoming = PulseTick;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: PulseTick) {
let _ = ctx.send(self.router, RouterMsg::Beat { seq: msg.seq });
}
}
struct RouterActor {
workers: Vec<ActorAddress>,
next: usize,
completed: u64,
}
impl RouterActor {
fn new() -> Self {
Self {
workers: Vec::new(),
next: 0,
completed: 0,
}
}
}
impl ActorInterface for RouterActor {
type Incoming = RouterMsg;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: RouterMsg) {
match msg {
RouterMsg::Configure { workers } => {
self.workers = workers;
self.next = 0;
}
RouterMsg::Beat { seq } => {
if self.workers.is_empty() {
return;
}
let burst = 48 + (seq as usize % 32);
for route in 0..burst {
let target = self.workers[self.next % self.workers.len()];
self.next = self.next.wrapping_add(1);
let _ = ctx.send(
target,
WorkItem {
seq,
route: route as u32,
hops_left: 1 + ((seq + route as u64) % 3) as u8,
},
);
}
}
RouterMsg::Complete { worker, seq, route } => {
self.completed = self.completed.wrapping_add(1);
if self.completed % 7 == 0 && !self.workers.is_empty() {
let target =
self.workers[(worker as usize + route as usize) % self.workers.len()];
let _ = ctx.send(
target,
WorkItem {
seq,
route: route.wrapping_add(1000),
hops_left: 1,
},
);
}
}
}
}
}
struct WorkerActor {
id: u32,
router: ActorAddress,
}
impl ActorInterface for WorkerActor {
type Incoming = WorkItem;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: WorkItem) {
if msg.hops_left > 0 {
let _ = ctx.send(
ctx.self_addr(),
WorkItem {
seq: msg.seq,
route: msg.route,
hops_left: msg.hops_left - 1,
},
);
return;
}
thread::sleep(WORK_ITEM_DELAY);
let _ = ctx.send(
self.router,
RouterMsg::Complete {
worker: self.id,
seq: msg.seq,
route: msg.route,
},
);
}
}
struct QueuedSinkActor;
impl ActorInterface for QueuedSinkActor {
type Incoming = WorkItem;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: WorkItem) {
ctx.suspend_self();
}
}
#[derive(Default)]
struct DashboardStatsHook {
actors: Mutex<HashMap<ActorAddress, ActorDetail>>,
}
#[derive(Clone)]
struct ActorDetail {
worker_id: usize,
mailbox_depth: usize,
last_msg_type: Option<String>,
messages_processed: u64,
poisoned: bool,
message_type_counts: Vec<(String, u64)>,
}
impl StatsHook for DashboardStatsHook {
fn on_tick(&self, worker_id: usize, snapshots: &[ActorSnapshot]) {
let mut actors = self.actors.lock();
for snapshot in snapshots {
actors.insert(
snapshot.address,
ActorDetail {
worker_id,
mailbox_depth: snapshot.mailbox_depth,
last_msg_type: snapshot.last_msg_type.map(str::to_owned),
messages_processed: snapshot.messages_processed,
poisoned: snapshot.poisoned,
message_type_counts: snapshot
.message_type_counts
.iter()
.map(|(name, count)| ((*name).to_owned(), *count))
.collect(),
},
);
}
}
}
impl DashboardStatsHook {
fn snapshot(
&self,
live_workers: &[(ActorAddress, usize)],
names: &HashMap<ActorAddress, String>,
) -> Vec<ActorDetailFrame> {
let actors = self.actors.lock();
live_workers
.iter()
.map(|(address, worker_id)| {
let detail = actors.get(address);
ActorDetailFrame {
address: address.to_string(),
name: names.get(address).cloned(),
worker_id: detail.map_or(*worker_id, |detail| detail.worker_id),
mailbox_depth: detail.map_or(0, |detail| detail.mailbox_depth),
last_msg_type: detail.and_then(|detail| detail.last_msg_type.clone()),
messages_processed: detail.map_or(0, |detail| detail.messages_processed),
poisoned: detail.is_some_and(|detail| detail.poisoned),
message_type_counts: detail
.map(|detail| detail.message_type_counts.clone())
.unwrap_or_default(),
}
})
.collect()
}
}
#[derive(Serialize)]
struct ActorDetailFrame {
address: String,
name: Option<String>,
worker_id: usize,
mailbox_depth: usize,
last_msg_type: Option<String>,
messages_processed: u64,
poisoned: bool,
message_type_counts: Vec<(String, u64)>,
}
fn main() {
let dashboard = start_dashboard(DashboardConfig::default());
dashboard.start_http_standalone();
let mut runtime = Runtime::new(RuntimeConfig {
num_threads: 4,
max_actors: 128,
channel_buffer_size: 4096,
actor_message_budget: 8,
});
let stats_hook = Arc::new(DashboardStatsHook::default());
runtime.set_stats_hook(stats_hook.clone());
let router = runtime.spawn(RouterActor::new()).expect("spawn router");
let mut names = HashMap::new();
names.insert(router, "router".to_owned());
let mut workers = Vec::with_capacity(WORKER_ACTORS);
for id in 0..WORKER_ACTORS {
let address = runtime
.spawn(WorkerActor {
id: id as u32,
router,
})
.expect("spawn worker actor");
names.insert(address, format!("worker-{id}"));
workers.push(address);
}
let pulse = runtime.spawn(PulseActor { router }).expect("spawn pulse");
names.insert(pulse, "pulse".to_owned());
let queued_sink = runtime.spawn(QueuedSinkActor).expect("spawn queued sink");
names.insert(queued_sink, "queued-sink".to_owned());
runtime
.send_to(
router,
RouterMsg::Configure {
workers: workers.clone(),
},
)
.expect("configure router");
let runtime = runtime.run().expect("start swactor runtime");
let stream = StreamId::new(NodeId::new(NODE_ID), Lifetime(1));
let mut position = 0_u64;
let mut seq = 0_u64;
let mut ticks_until_publish = 0_u8;
println!(
"dashboard listening at http://127.0.0.1:{}/view/swactor/workers",
DashboardConfig::default().port
);
println!(
"dummy node {NODE_ID} running {} swactor actors",
names.len()
);
loop {
let _ = runtime.runtime.send_to(pulse, PulseTick { seq });
if seq % 2 == 0 {
let _ = runtime.runtime.send_to(
queued_sink,
WorkItem {
seq,
route: u32::MAX,
hops_left: 0,
},
);
}
seq = seq.wrapping_add(1);
if ticks_until_publish == 0 {
publish_runtime_snapshot(
&dashboard,
&stream,
&mut position,
&runtime.runtime,
&stats_hook,
&names,
);
ticks_until_publish = (PUBLISH_INTERVAL.as_millis() / PULSE_INTERVAL.as_millis()) as u8;
}
ticks_until_publish = ticks_until_publish.saturating_sub(1);
thread::sleep(PULSE_INTERVAL);
}
}
fn publish_runtime_snapshot(
dashboard: &DashboardHandle,
stream: &StreamId,
position: &mut u64,
runtime: &Runtime,
stats_hook: &DashboardStatsHook,
names: &HashMap<ActorAddress, String>,
) {
let stats = runtime.stats();
let actor_details = stats_hook.snapshot(&stats.actors, names);
let actors: Vec<Value> = stats
.actors
.iter()
.map(|(address, worker_id)| json!([address.to_string(), worker_id]))
.collect();
let workers = serde_json::to_value(&stats.workers).expect("serialize worker stats");
let actor_details = serde_json::to_value(actor_details).expect("serialize actor stats");
let tick_timings = serde_json::to_value(&stats.tick_timings).expect("serialize tick timings");
let total_mailbox_depth: usize = stats
.workers
.iter()
.map(|worker| worker.mailbox_depth)
.sum();
publish_json(
dashboard,
stream,
position,
RUNTIME_STATS,
json!({
"num_workers": stats.num_workers,
"uptime_ms": stats.uptime_ms,
"actors_live": stats.actors.len(),
"mailbox_depth": total_mailbox_depth,
"actors": actors,
"workers": workers,
"actor_details": actor_details,
"tick_timings": tick_timings,
}),
);
publish_json(
dashboard,
stream,
position,
RUNTIME_WORKERS,
json!({ "workers": stats.workers }),
);
publish_json(
dashboard,
stream,
position,
RUNTIME_ACTORS,
json!({ "actors": actor_details }),
);
}
fn publish_json(
dashboard: &DashboardHandle,
stream: &StreamId,
position: &mut u64,
channel: &str,
value: Value,
) {
let payload = serde_json::to_vec(&value).expect("serialize dashboard frame");
let frame = Frame::new(
ChannelId(*position as u32 + 1),
Position(*position),
payload,
);
dashboard.publish(FrameEvent {
stream: StreamEvent {
node: stream.node.as_str().to_string(),
life: stream.life.0,
},
channel: channel.to_owned(),
position: *position,
payload: frame.payload.clone(),
});
*position = position.wrapping_add(1);
}

View file

@ -97,8 +97,6 @@ impl DashboardHandle {
views.register(Arc::new(live_explorer::LiveDatastreamExplorer::default())); views.register(Arc::new(live_explorer::LiveDatastreamExplorer::default()));
views.register(Arc::new(hardware_view::HardwareDashboardView::default())); views.register(Arc::new(hardware_view::HardwareDashboardView::default()));
views.register(swactor::worker_view()); views.register(swactor::worker_view());
views.register(swactor::actor_overview_view());
views.register(swactor::actor_dossier_view());
let store = Arc::new(DashboardStore::new( let store = Arc::new(DashboardStore::new(
config.raw_frame_history, config.raw_frame_history,
Arc::clone(&views), Arc::clone(&views),

View file

@ -1,185 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Actor dossier — swactor</title>
<style>
:root{
--bg:#0f172a; --panel:#1e293b; --panel2:#172033; --border:#334155;
--text:#e2e8f0; --muted:#94a3b8; --accent:#60a5fa;
--running:#34d399; --poisoned:#f87171;
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
}
*{box-sizing:border-box}
body{margin:0;padding:0;background:var(--bg);color:var(--text);}
.mono,.table,th,td,.value,.num{font-variant-numeric:tabular-nums;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
header.bar{display:flex;align-items:center;gap:16px;padding:14px 20px;border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:0;z-index:5;}
header.bar h1{font-size:18px;margin:0;font-weight:600;letter-spacing:.01em;}
nav.tabs{display:flex;gap:4px;margin-left:auto;}
nav.tabs a{padding:6px 12px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:13px;border:1px solid transparent;}
nav.tabs a:hover{color:var(--text);background:var(--panel2);}
nav.tabs a.cur{color:var(--accent);background:var(--panel2);border-color:var(--border);}
select{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;}
.wrap{padding:20px;max-width:1100px;margin:0 auto;}
.picker{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:18px;}
.picker label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;}
#pick{min-width:300px;max-width:560px;flex:1;}
.back-link{font-size:13px;}
.badge{display:inline-block;padding:2px 8px;border-radius:99px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:600;}
.empty{padding:28px;border:1px dashed #475569;border-radius:12px;color:var(--muted);text-align:center;}
.toast{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);background:var(--panel2);border:1px solid var(--border);color:var(--text);padding:10px 16px;border-radius:10px;font-size:13px;max-width:min(90vw,560px);text-align:center;z-index:50;box-shadow:0 8px 24px rgba(0,0,0,.45);}
.toast.err{border-color:var(--poisoned);color:var(--poisoned);}
.kvs{display:grid;grid-template-columns:max-content 1fr;gap:6px 14px;}
.kvs .k{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.05em;}
.kvs .v{font-size:13px;word-break:break-word;}
.section{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px 18px;margin-bottom:16px;}
h2{font-size:14px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:0 0 12px;}
.mb-dyn{display:flex;gap:24px;align-items:center;flex-wrap:wrap;}
.mb-spark{flex:1 1 320px;min-width:0;}
.mb-spark .spark{display:block;width:100%;height:120px;}
.mb-vals{display:flex;flex-direction:column;gap:16px;min-width:150px;}
.mb-val .label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
.mb-val .value{font-size:28px;margin-top:4px;font-weight:600;line-height:1;}
.bar-row{display:flex;align-items:center;gap:8px;margin:5px 0;}
.bar-row .name{width:48%;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.bar-track{flex:1;height:10px;background:var(--panel2);border-radius:99px;overflow:hidden;}
.bar-fill{height:100%;background:var(--accent);border-radius:99px;}
.bar-row .num{width:90px;text-align:right;color:var(--muted);}
.bar-row.last .bar-fill{background:var(--running);}
.bar-row.last{font-weight:600;}
a{color:var(--accent);text-decoration:none;}
.stale{opacity:.55;}
</style>
</head>
<body>
<header class="bar">
<h1>Actor dossier</h1>
<select id="runtime"></select>
<nav class="tabs">
<a href="/view/swactor/actor-overview">Overview</a>
<a href="/view/swactor/actor-dossier" class="cur">Dossier</a>
<a href="/view/swactor/workers">Workers</a>
</nav>
</header>
<div class="wrap">
<div class="picker">
<label for="pick">Actor</label>
<select id="pick"></select>
<a class="back-link" href="/view/swactor/actor-overview">&larr; back to overview</a>
</div>
<div id="empty" class="toast" hidden>No runtime actor frames yet.</div>
<div id="content"></div>
</div>
<script>
const POLL_MS=750;
const STATE_COLOR={running:'#34d399',poisoned:'#f87171'};
const elRuntime=document.getElementById('runtime');
const elPick=document.getElementById('pick');
const elEmpty=document.getElementById('empty');
const elContent=document.getElementById('content');
let runtimes=[],runtimeKey='',selectedAddr='';
function fmt(n,d=0){if(n==null||n===undefined)return'\u2014';if(typeof n!=='number')return String(n);return n.toLocaleString(undefined,{maximumFractionDigits:d});}
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
function shortAddr(a){a=String(a||'');return a.length<=12?a:a.slice(0,6)+'\u2026'+a.slice(-4);}
function urlActor(){try{return new URLSearchParams(location.search).get('actor');}catch(e){return null;}}
function stateBadge(a){const s=a.poisoned?'poisoned':'running';const c=STATE_COLOR[s];return '<span class="badge" style="color:'+c+';background:'+c+'22">'+s+'</span>';}
function growthColor(g){if(g>1.5)return '#f87171';if(g>0.3)return '#fbbf24';return '#34d399';}
function areaChart(data,w,h,color){
if(!data||data.length<2)return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="100%" height="'+h+'" preserveAspectRatio="none"></svg>';
const max=Math.max(1,...data);const n=data.length;
let line='',area='0,'+h+' ';
for(let i=0;i<n;i++){const x=(i/(n-1)*w).toFixed(1);const y=(h-(data[i]/max)*(h-2)-1).toFixed(1);line+=x+','+y+' ';area+=x+','+y+' ';}
area+=' '+w+','+h;
return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="100%" height="'+h+'" preserveAspectRatio="none">'+
'<polygon points="'+area.trim()+'" fill="'+(color||'#60a5fa')+'22"/>'+
'<polyline points="'+line.trim()+'" fill="none" stroke="'+(color||'#60a5fa')+'" stroke-width="1.5"/></svg>';
}
function kv(k,vhtml){return '<div class="k">'+esc(k)+'</div><div class="v">'+vhtml+'</div>';}
function typeShort(t){t=String(t||'');const i=t.lastIndexOf('::');return i>=0?t.slice(i+2):t;}
function pickRuntime(list){
if(!runtimeKey||!list.some(r=>r.stream.key===runtimeKey)){
const live=list.find(r=>r.live)||list[0];runtimeKey=live?live.stream.key:'';
}
return list.find(r=>r.stream.key===runtimeKey)||null;
}
function currentActors(rt){return rt?(rt.actors||[]):[];}
function findActor(rt){
const actors=currentActors(rt);
if(selectedAddr){const hit=actors.find(a=>a.address===selectedAddr);if(hit)return hit;}
return actors[0]||null;
}
function renderPicker(rt){
const actors=currentActors(rt);
elPick.innerHTML=actors.map(a=>
'<option value="'+esc(a.address)+'">'+esc(shortAddr(a.address))+(a.name?' \u00b7 '+esc(a.name):'')+'</option>'
).join('');
const cur=findActor(rt);
if(cur){selectedAddr=cur.address;elPick.value=selectedAddr;}
}
function renderRuntimeSelector(){
elRuntime.innerHTML=runtimes.map(r=>
'<option value="'+esc(r.stream.key)+'">'+esc(r.stream.node)+'#'+r.stream.life+(r.live?'':' (stale)')+'</option>'
).join('');
if(runtimeKey)elRuntime.value=runtimeKey;
}
function render(a,rt){
if(!a){elContent.innerHTML='<div class="empty">Select an actor. None are present on the current frame.</div>';return;}
const lastType=a.last_msg_type||'\u2014';
const diet=(a.message_type_counts||[]).slice();
diet.sort((x,y)=>y.count-x.count);
const dmax=Math.max(1,...diet.map(d=>d.count));
const lastName=a.last_msg_type;
const dietHtml=diet.length?diet.map(d=>{
const cls=d.message_type===lastName?'bar-row last':'bar-row';
return '<div class="'+cls+'"><span class="name">'+esc(d.message_type)+'</span>'+
'<div class="bar-track"><div class="bar-fill" style="width:'+(d.count/dmax*100).toFixed(1)+'%"></div></div>'+
'<span class="num">'+fmt(d.count)+'</span></div>';
}).join(''):'<div class="empty">no message-type counts on frame</div>';
const hist=(a.history||[]).map(h=>h.mailbox_depth);
const g=a.mailbox_growth||0;
elContent.innerHTML=
'<div class="section"><h2>Identity &amp; lifecycle</h2><div class="kvs">'+
kv('address','<span class="mono">'+esc(a.address)+'</span>')+
(a.name?kv('name',esc(a.name)):'')+
(a.actor_type?kv('actor type','<span class="mono" title="'+esc(a.actor_type)+'">'+esc(typeShort(a.actor_type))+'</span>'):'')+
(a.message_type?kv('message type','<span class="mono" title="'+esc(a.message_type)+'">'+esc(typeShort(a.message_type))+'</span>'):'')+
kv('state',stateBadge(a))+
kv('worker',a.worker_id==null?'\u2014':esc(a.worker_id))+
kv('last message',esc(lastType))+
kv('last seen',a.last_seen_ms_ago!=null?(a.last_seen_ms_ago/1000).toFixed(1)+'s ago':'\u2014')+
'</div></div>'+
'<div class="section"><h2>Mailbox dynamics</h2><div class="mb-dyn">'+
'<div class="mb-spark">'+areaChart(hist,720,120,growthColor(g))+'</div>'+
'<div class="mb-vals">'+
'<div class="mb-val"><div class="label">depth</div><div class="value" style="color:'+growthColor(g)+'">'+fmt(a.mailbox_depth)+'</div></div>'+
'<div class="mb-val"><div class="label">growth</div><div class="value">'+fmt(g,1)+'/s</div></div>'+
'<div class="mb-val"><div class="label">msg/s</div><div class="value">'+fmt(a.msg_per_sec,1)+'</div></div>'+
'</div>'+
'</div></div>'+
'<div class="section"><h2>Message diet</h2>'+dietHtml+'</div>';
}
async function refresh(){
try{
const res=await fetch('/api/view/swactor/actor-dossier',{cache:'no-store'});
if(!res.ok)throw new Error('HTTP '+res.status);
const data=await res.json();
runtimes=data.runtimes||[];
renderRuntimeSelector();
const rt=pickRuntime(runtimes);
if(rt&&currentActors(rt).length>0){elEmpty.hidden=true;}else{elEmpty.className='toast';elEmpty.textContent='No runtime actor frames yet.';elEmpty.hidden=false;}
renderPicker(rt);
render(findActor(rt),rt);
}catch(e){
elEmpty.className='toast err';elEmpty.hidden=false;elEmpty.textContent='Failed to load actor frames: '+e.message;elContent.innerHTML='';
}
}
elRuntime.addEventListener('change',e=>{runtimeKey=e.target.value;const rt=pickRuntime(runtimes);renderPicker(rt);render(findActor(rt),rt);});
elPick.addEventListener('change',e=>{selectedAddr=e.target.value;const rt=pickRuntime(runtimes);render(findActor(rt),rt);});
const q=urlActor();if(q)selectedAddr=q;
refresh();
setInterval(refresh,POLL_MS);
</script>
</body>
</html>

View file

@ -1,312 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Actor overview — swactor</title>
<style>
:root{
--bg:#0f172a; --panel:#1e293b; --panel2:#172033; --border:#334155;
--text:#e2e8f0; --muted:#94a3b8; --accent:#60a5fa;
--running:#34d399; --poisoned:#f87171;
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
}
*{box-sizing:border-box}
body{margin:0;padding:0;background:var(--bg);color:var(--text);}
.mono,.table,th,td,.value,.num{font-variant-numeric:tabular-nums;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
header.bar{display:flex;align-items:center;gap:16px;padding:14px 20px;border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:0;z-index:5;}
header.bar h1{font-size:18px;margin:0;font-weight:600;letter-spacing:.01em;}
nav.tabs{display:flex;gap:4px;margin-left:auto;}
nav.tabs a{padding:6px 12px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:13px;border:1px solid transparent;}
nav.tabs a:hover{color:var(--text);background:var(--panel2);}
nav.tabs a.cur{color:var(--accent);background:var(--panel2);border-color:var(--border);}
select{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;}
.wrap{padding:20px;max-width:1400px;margin:0 auto;}
.grid{display:grid;gap:12px;}
.cards{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin-bottom:16px;}
.card{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:14px;}
.card .label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
.card .value{font-size:26px;margin-top:6px;font-weight:600;}
.card .value.bad{color:var(--poisoned);}
.badge{display:inline-block;padding:2px 8px;border-radius:99px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:600;}
.table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden;}
.table th,.table td{padding:8px 10px;border-bottom:1px solid #1f2937;text-align:left;white-space:nowrap;}
.table th{color:#93c5fd;background:var(--panel2);cursor:pointer;user-select:none;font-size:12px;text-transform:uppercase;letter-spacing:.05em;}
.table th:hover{color:#fff;}
.table tr{cursor:pointer;}
.table tbody tr:hover{background:#1d4ed822;}
.table td.muted,.muted{color:var(--muted);}
.spark{display:block;}
.bar-row{display:flex;align-items:center;gap:8px;margin:5px 0;}
.bar-row .name{width:38%;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.bar-track{flex:1;height:10px;background:var(--panel2);border-radius:99px;overflow:hidden;}
.bar-fill{height:100%;background:var(--accent);border-radius:99px;}
.bar-row .num{width:90px;text-align:right;color:var(--muted);}
input{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:13px;}
.filters{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px;}
.filters label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;}
h2{font-size:14px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:22px 0 10px;}
.empty{padding:28px;border:1px dashed #475569;border-radius:12px;color:var(--muted);text-align:center;}
.toast{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);background:var(--panel2);border:1px solid var(--border);color:var(--text);padding:10px 16px;border-radius:10px;font-size:13px;max-width:min(90vw,560px);text-align:center;z-index:50;box-shadow:0 8px 24px rgba(0,0,0,.45);}
.toast.err{border-color:var(--poisoned);color:var(--poisoned);}
.stacked{display:flex;height:18px;border-radius:6px;overflow:hidden;border:1px solid var(--border);}
.legend{display:flex;gap:14px;flex-wrap:wrap;margin-top:8px;font-size:12px;color:var(--muted);}
.legend i{display:inline-block;width:12px;height:12px;margin-right:5px;border-radius:3px;vertical-align:middle;}
a{color:var(--accent);text-decoration:none;}
.roster-box{max-height:520px;overflow:auto;border:1px solid var(--border);border-radius:12px;}
.roster-box .table{border-radius:0;border:none;}
.stale{opacity:.55;}
</style>
</head>
<body>
<header class="bar">
<h1>Actor overview</h1>
<select id="runtime"></select>
<nav class="tabs">
<a href="/view/swactor/actor-overview" class="cur">Overview</a>
<a href="/view/swactor/actor-dossier">Dossier</a>
<a href="/view/swactor/workers">Workers</a>
</nav>
</header>
<div class="wrap">
<div id="empty" class="toast" hidden>No runtime actor frames yet. A live swactor runtime publishing <code>runtime.actors</code> will populate this page.</div>
<div id="cards" class="grid cards"></div>
<h2>State distribution</h2>
<div class="stacked" id="dist"></div>
<div class="legend" id="distLegend"></div>
<h2>Throughput</h2>
<div id="throughput"></div>
<h2>Worker balance</h2>
<div id="workers"></div>
<h2>Hot actors — mailbox growth</h2>
<div id="hot"></div>
<h2>Alerts</h2>
<div id="alerts"></div>
<h2>Roster</h2>
<div class="filters">
<label for="f-search">search</label>
<input id="f-search" type="search" placeholder="name · addr · worker" autocomplete="off" spellcheck="false">
<label for="f-state">state</label>
<select id="f-state">
<option value="all">all states</option>
<option value="running">running</option>
<option value="poisoned">poisoned</option>
</select>
<label for="f-worker">worker</label>
<select id="f-worker"></select>
</div>
<div class="roster-box">
<table class="table">
<thead><tr id="head"></tr></thead>
<tbody id="rows"></tbody>
</table>
</div>
</div>
<script>
const POLL_MS=750;
const elRuntime=document.getElementById('runtime');
const elEmpty=document.getElementById('empty');
const elCards=document.getElementById('cards');
const elDist=document.getElementById('dist');
const elDistLegend=document.getElementById('distLegend');
const elThroughput=document.getElementById('throughput');
const elWorkers=document.getElementById('workers');
const elHot=document.getElementById('hot');
const elAlerts=document.getElementById('alerts');
const elHead=document.getElementById('head');
const elRows=document.getElementById('rows');
const elSearch=document.getElementById('f-search');
const elState=document.getElementById('f-state');
const elWorker=document.getElementById('f-worker');
const STATE_COLOR={running:'#34d399',poisoned:'#f87171'};
const COLS=[['actor','Actor'],['type','Type'],['state','State'],['mailbox','Mailbox'],['rate','Msg/s'],
['processed','Processed'],['last','Last msg'],['worker','Worker']];
const DEFDIR={actor:'asc',type:'asc',state:'asc',mailbox:'desc',rate:'desc',processed:'desc',last:'asc',worker:'asc'};
let sortKey='mailbox',sortDir='desc';
let fSearch='',fState='all',fWorker='all';
let runtimes=[],runtimeKey='';
function fmt(n,d=0){if(n==null||n===undefined)return'\u2014';if(typeof n!=='number')return String(n);return n.toLocaleString(undefined,{maximumFractionDigits:d});}
function ago(sec){sec=Math.max(0,Math.round(sec));if(sec<60)return sec+'s';const m=Math.floor(sec/60);if(m<60)return m+'m';return Math.floor(m/60)+'h';}
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
function shortAddr(a){a=String(a||'');return a.length<=12?a:a.slice(0,6)+'\u2026'+a.slice(-4);}
function typeShort(t){t=String(t||'');const i=t.lastIndexOf('::');return i>=0?t.slice(i+2):t;}
function stateBadge(a){const s=a.poisoned?'poisoned':'running';const c=STATE_COLOR[s];return '<span class="badge" style="color:'+c+';background:'+c+'22">'+s+'</span>';}
function growthColor(g){if(g>1.5)return '#f87171';if(g>0.3)return '#fbbf24';return '#34d399';}
function sparkline(data,w,h,color){
if(!data||data.length<2)return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="'+w+'" height="'+h+'"></svg>';
const max=Math.max(1,...data);const n=data.length;
let pts='';for(let i=0;i<n;i++)pts+=(i/(n-1)*w).toFixed(1)+','+(h-(data[i]/max)*(h-2)-1).toFixed(1)+' ';
return '<svg class="spark" viewBox="0 0 '+w+' '+h+'" width="'+w+'" height="'+h+'" preserveAspectRatio="none"><polyline points="'+pts.trim()+'" fill="none" stroke="'+(color||'#60a5fa')+'" stroke-width="1.5"/></svg>';
}
function pickRuntime(list){
if(!runtimeKey||!list.some(r=>r.stream.key===runtimeKey)){
const live=list.find(r=>r.live)||list[0];
runtimeKey=live?live.stream.key:'';
}
return list.find(r=>r.stream.key===runtimeKey)||null;
}
function renderRuntimeSelector(){
elRuntime.innerHTML=runtimes.map(r=>
'<option value="'+esc(r.stream.key)+'">'+esc(r.stream.node)+'#'+r.stream.life+(r.live?'':' (stale)')+'</option>'
).join('');
if(runtimeKey)elRuntime.value=runtimeKey;
}
function renderCards(rt){
const s=rt.summary||{};
const uptime=s.uptime_ms!=null?ago(s.uptime_ms/1000):'\u2014';
elCards.innerHTML=
'<div class="card"><div class="label">Actors</div><div class="value">'+fmt(s.actors)+'</div></div>'+
'<div class="card"><div class="label">msg/s</div><div class="value">'+fmt(s.msg_per_sec,1)+'</div></div>'+
'<div class="card"><div class="label">Mailbox</div><div class="value">'+fmt(s.mailbox_depth)+'</div></div>'+
'<div class="card"><div class="label">Poisoned</div><div class="value'+(s.poisoned>0?' bad':'')+'">'+fmt(s.poisoned)+'</div></div>'+
'<div class="card"><div class="label">Uptime</div><div class="value">'+uptime+'</div></div>';
}
function renderDist(rt){
const actors=rt.actors||[];const n=actors.length||1;
let running=0,poisoned=0;
actors.forEach(a=>{if(a.poisoned)poisoned++;else running++;});
let segs='';
if(running)segs+='<div style="width:'+(running/n*100).toFixed(2)+'%;background:'+STATE_COLOR.running+';"></div>';
if(poisoned)segs+='<div style="width:'+(poisoned/n*100).toFixed(2)+'%;background:'+STATE_COLOR.poisoned+';"></div>';
elDist.innerHTML=segs||'<div style="width:100%;background:var(--panel2);"></div>';
elDistLegend.innerHTML='<span><i style="background:'+STATE_COLOR.running+'"></i>running: '+running+'</span>'+
'<span><i style="background:'+STATE_COLOR.poisoned+'"></i>poisoned: '+poisoned+'</span>';
}
function renderThroughput(rt){
const h=(rt.history||[]).map(x=>x.msg_per_sec);
const mb=(rt.history||[]).map(x=>x.mailbox_depth);
elThroughput.innerHTML=
sparkline(h,760,90,'#60a5fa')+'<div style="height:6px"></div>'+
sparkline(mb,760,90,'#fbbf24')+
'<div class="legend"><span><i style="background:#60a5fa"></i>msg/s</span><span><i style="background:#fbbf24"></i>mailbox</span></div>';
}
function renderWorkers(rt){
const counts={};
(rt.actors||[]).forEach(a=>{const w=a.worker_id==null?'?':a.worker_id;counts[w]=(counts[w]||0)+1;});
const keys=Object.keys(counts).sort();
const max=Math.max(1,...Object.values(counts));
elWorkers.innerHTML=keys.map(w=>{
const c=counts[w];
return '<div class="bar-row"><span class="name">worker '+esc(w)+'</span>'+
'<div class="bar-track"><div class="bar-fill" style="width:'+(c/max*100).toFixed(1)+'%"></div></div>'+
'<span class="num">'+c+'</span></div>';
}).join('')||'<div class="empty">no worker placement on frame</div>';
}
function renderHot(rt){
const hot=(rt.actors||[]).slice().sort((a,b)=>(b.mailbox_growth||0)-(a.mailbox_growth||0)).slice(0,8);
const gmax=Math.max(1,...hot.map(a=>Math.max(0,a.mailbox_growth||0)));
elHot.innerHTML=hot.map(a=>{
const g=a.mailbox_growth||0;const gc=growthColor(g);
return '<div class="bar-row" data-addr="'+esc(a.address)+'">'+
'<span class="name">'+shortAddr(a.address)+(a.name?' '+esc(a.name):'')+'</span>'+
'<div class="bar-track"><div class="bar-fill" style="width:'+(Math.max(0,g)/gmax*100).toFixed(1)+'%;background:'+gc+'"></div></div>'+
'<span class="num">'+fmt(g,1)+'/s</span></div>';
}).join('')||'<div class="empty">no actors</div>';
}
function renderAlerts(rt){
const alerted=(rt.actors||[]).filter(a=>a.poisoned||(a.mailbox_growth||0)>1.5);
if(!alerted.length){elAlerts.innerHTML='<div class="empty">fleet healthy</div>';return;}
alerted.sort((a,b)=>{const ap=a.poisoned?0:1,bp=b.poisoned?0:1;if(ap!==bp)return ap-bp;return (b.mailbox_growth||0)-(a.mailbox_growth||0);});
elAlerts.innerHTML='<table class="table"><tbody>'+
alerted.map(a=>{
const reason=a.poisoned?'poisoned':'saturating';
return '<tr data-addr="'+esc(a.address)+'"><td>'+shortAddr(a.address)+(a.name?' '+esc(a.name):'')+' '+stateBadge(a)+'</td>'+
'<td class="muted">'+reason+'</td><td style="text-align:right;color:var(--muted);width:1%;">\u2192</td></tr>';
}).join('')+'</tbody></table>';
}
function colValue(a,key){
switch(key){
case 'actor':return (a.name||a.address).toLowerCase();
case 'type':return typeShort(a.actor_type).toLowerCase();
case 'state':return a.poisoned?'poisoned':'running';
case 'mailbox':return a.mailbox_depth||0;
case 'rate':return a.msg_per_sec||0;
case 'processed':return a.messages_processed||0;
case 'last':return a.last_msg_type||'';
case 'worker':return a.worker_id==null?-1:a.worker_id;
}
return 0;
}
function visible(rt){
let list=rt.actors||[];
if(fSearch){const q=fSearch.toLowerCase();list=list.filter(a=>((a.name?a.name+' ':'')+(a.actor_type?typeShort(a.actor_type)+' '+a.actor_type+' ':'')+a.address+' '+(a.worker_id==null?'':a.worker_id)).toLowerCase().indexOf(q)>=0);}
if(fState!=='all')list=list.filter(a=>(a.poisoned?'poisoned':'running')===fState);
if(fWorker!=='all')list=list.filter(a=>String(a.worker_id)===fWorker);
const k=sortKey,dir=sortDir==='asc'?1:-1;
return list.slice().sort((x,y)=>{const vx=colValue(x,k),vy=colValue(y,k);let r;if(typeof vx==='number'&&typeof vy==='number')r=vx-vy;else r=String(vx).localeCompare(String(vy));return r*dir;});
}
function renderHead(){
elHead.innerHTML=COLS.map(c=>{
const key=c[0],label=c[1];
const arrow=sortKey===key?(sortDir==='asc'?' \u25B2':' \u25BC'):'';
const order=sortKey===key?(sortDir==='asc'?'ascending':'descending'):'none';
return '<th data-key="'+key+'" scope="col" aria-sort="'+order+'">'+label+arrow+'</th>';
}).join('');
}
function populateWorkerFilter(rt){
const seen={};(rt.actors||[]).forEach(a=>{if(a.worker_id!=null)seen[a.worker_id]=1;});
const keys=Object.keys(seen).sort((a,b)=>a-b);
elWorker.innerHTML='<option value="all">all workers</option>'+keys.map(w=>'<option value="'+esc(w)+'">'+esc(w)+'</option>').join('');
if(fWorker!=='all'&&!keys.includes(fWorker))fWorker='all';
elWorker.value=fWorker;
}
function renderRows(rt){
const list=visible(rt);
if(!list.length){elRows.innerHTML='<tr><td colspan="'+COLS.length+'"><div class="empty">No actors match the current filters.</div></td></tr>';return;}
elRows.innerHTML=list.map(a=>{
const hist=(a.history||[]).map(h=>h.mailbox_depth);
const gc=growthColor(a.mailbox_growth||0);
const mbox='<span style="display:inline-flex;align-items:center;gap:8px">'+
'<span style="color:'+gc+';font-weight:600">'+fmt(a.mailbox_depth)+'</span>'+sparkline(hist,60,16,gc)+'</span>';
const actorCell=shortAddr(a.address)+(a.name?'<br><span class="muted" style="font-size:11px">'+esc(a.name)+'</span>':'');
const typeCell=a.actor_type?'<span class="muted" title="'+esc(a.actor_type)+'">'+esc(typeShort(a.actor_type))+'</span>':'<span class="muted">\u2014</span>';
return '<tr data-addr="'+esc(a.address)+'">'+
'<td>'+actorCell+'</td><td>'+typeCell+'</td><td>'+stateBadge(a)+'</td><td>'+mbox+'</td>'+
'<td>'+fmt(a.msg_per_sec,1)+'</td><td>'+fmt(a.messages_processed)+'</td>'+
'<td><span class="muted" style="display:inline-block;max-width:240px;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom">'+esc(a.last_msg_type)+'</span></td>'+
'<td>'+(a.worker_id==null?'<span class="muted">\u2014</span>':a.worker_id)+'</td></tr>';
}).join('');
}
function render(rt){
if(!rt){[elCards,elDist,elThroughput,elWorkers,elHot,elAlerts,elRows].forEach(e=>e.innerHTML='');return;}
elEmpty.hidden=true;
renderCards(rt);renderDist(rt);renderThroughput(rt);renderWorkers(rt);renderHot(rt);renderAlerts(rt);
populateWorkerFilter(rt);renderHead();renderRows(rt);
}
async function refresh(){
try{
const res=await fetch('/api/view/swactor/actor-overview',{cache:'no-store'});
if(!res.ok)throw new Error('HTTP '+res.status);
const data=await res.json();
runtimes=data.runtimes||[];
renderRuntimeSelector();
const rt=pickRuntime(runtimes);
if(rt){elEmpty.hidden=true;}else{elEmpty.className='toast';elEmpty.textContent='No runtime actor frames yet. A live swactor runtime publishing runtime.actors will populate this page.';elEmpty.hidden=false;}
render(rt);
}catch(e){
elEmpty.className='toast err';
elEmpty.textContent='Failed to load actor frames: '+e.message;
elEmpty.hidden=false;
}
}
function setSort(key){if(sortKey===key)sortDir=sortDir==='asc'?'desc':'asc';else{sortKey=key;sortDir=DEFDIR[key]||'asc';}renderHead();renderRows(pickRuntime(runtimes));}
elRuntime.addEventListener('change',e=>{runtimeKey=e.target.value;render(pickRuntime(runtimes));});
elSearch.addEventListener('input',e=>{fSearch=e.target.value.toLowerCase();renderRows(pickRuntime(runtimes));});
elState.addEventListener('change',e=>{fState=e.target.value;renderRows(pickRuntime(runtimes));});
elWorker.addEventListener('change',e=>{fWorker=e.target.value;renderRows(pickRuntime(runtimes));});
elHead.addEventListener('click',e=>{const th=e.target.closest('th[data-key]');if(th)setSort(th.getAttribute('data-key'));});
document.addEventListener('click',e=>{const el=e.target.closest('[data-addr]');if(el)location.href='/view/swactor/actor-dossier?actor='+encodeURIComponent(el.getAttribute('data-addr'));});
renderHead();
refresh();
setInterval(refresh,POLL_MS);
</script>
</body>
</html>

View file

@ -1,635 +0,0 @@
//! Actor-centric read-only view over `runtime.actors` / `runtime.stats` frames.
//!
//! Pure frame consumer: it folds incoming per-actor snapshots into local state
//! and exposes JSON + HTML. It sends nothing back to observed runtimes. Parsing
//! is intentionally tolerant of publisher shape — a frame is a flat record that
//! may come from the per-worker `DatastreamStatsHook` envelope
//! (`{worker_id, actors:[...]}`) or from a process that publishes a merged
//! `{actors:[...]}` payload (the dashboard dummy node, app runtimes). Fields the
//! publisher includes (name, worker_id, lifecycle flags) are displayed; ones it
//! omits are left blank rather than fabricated.
//!
//! Reliably on the frame today: address, mailbox depth, throughput, last
//! message, poisoned flag, and the message-type diet. `worker_id` and `name`
//! appear when the publisher sends them. Finer lifecycle granularity
//! (new/suspended/stopping), actor Rust type, and spawn age are not on the
//! current frame and are therefore not shown — enriching the feed is a separate
//! core concern, not a dashboard one.
//!
//! Two pages share one data model: the fused overview+roster
//! (`/view/swactor/actor-overview`) and the per-actor dossier
//! (`/view/swactor/actor-dossier`). Each is a self-contained `DashboardView`
//! instance ingesting the same channels.
use std::collections::{BTreeMap, VecDeque};
use std::time::{Duration, Instant};
use datastream::frame::{Frame, StreamId};
use parking_lot::RwLock;
use serde::Serialize;
use serde_json::{Value, json};
use crate::swactor::{RUNTIME_ACTORS, RUNTIME_STATS};
use crate::view::DashboardView;
use crate::{FrameEvent, StreamEvent};
const CHANNELS: &[&str] = &[RUNTIME_ACTORS, RUNTIME_STATS];
const HISTORY_CAP: usize = 512;
const HISTORY_MIN_INTERVAL: Duration = Duration::from_millis(250);
const PER_ACTOR_HISTORY_CAP: usize = 120;
const GROWTH_WINDOW: usize = 12;
const LIVE_TTL: Duration = Duration::from_secs(8);
/// Read-only actor panel. One instance per served page so each page owns its
/// state independently; both ingest the same frames.
pub struct ActorPanelView {
id: &'static str,
title: &'static str,
path: &'static str,
html: &'static str,
state: RwLock<PanelState>,
}
impl ActorPanelView {
/// Fused overview + roster at `/view/swactor/actor-overview`.
pub fn overview() -> Self {
Self::new(
"swactor-actor-overview",
"Actor overview",
"swactor/actor-overview",
include_str!("actor_overview.html"),
)
}
/// Per-actor dossier at `/view/swactor/actor-dossier`.
pub fn dossier() -> Self {
Self::new(
"swactor-actor-dossier",
"Actor dossier",
"swactor/actor-dossier",
include_str!("actor_dossier.html"),
)
}
fn new(id: &'static str, title: &'static str, path: &'static str, html: &'static str) -> Self {
Self {
id,
title,
path,
html,
state: RwLock::new(PanelState::default()),
}
}
}
#[derive(Default)]
struct PanelState {
runtimes: BTreeMap<String, RuntimeState>,
}
struct RuntimeState {
stream: StreamEvent,
last_seen: Instant,
num_workers: Option<u32>,
uptime_ms: Option<u64>,
actors: BTreeMap<String, ActorState>,
history: VecDeque<HistorySample>,
}
impl RuntimeState {
fn new(stream: StreamEvent, now: Instant) -> Self {
Self {
stream,
last_seen: now,
num_workers: None,
uptime_ms: None,
actors: BTreeMap::new(),
history: VecDeque::with_capacity(HISTORY_CAP),
}
}
fn update(&mut self, channel: &str, payload: &[u8], now: Instant) {
self.last_seen = now;
let Ok(value) = serde_json::from_slice::<Value>(payload) else {
return;
};
match channel {
RUNTIME_ACTORS => self.apply_actors(&value, now),
RUNTIME_STATS => self.apply_stats(&value, now),
_ => {}
}
self.push_history(now);
}
fn apply_actors(&mut self, value: &Value, now: Instant) {
// Per-worker `worker_id` wrapper (DatastreamStatsHook shape) is the
// default placement for actors that do not carry one inline.
let wrapper_worker = u32_field(value, &["worker_id", "worker"]);
if let Some(actors) = value.get("actors").and_then(Value::as_array) {
for actor in actors {
self.apply_actor(actor, now, wrapper_worker);
}
return;
}
// A bare actor object per frame (no envelope).
self.apply_actor(value, now, wrapper_worker);
}
fn apply_actor(&mut self, value: &Value, now: Instant, default_worker: Option<u32>) {
let Some(address) = string_field(value, &["address", "addr", "actor_addr"]) else {
return;
};
let actor = self
.actors
.entry(address.clone())
.or_insert_with(|| ActorState::new(address));
actor.apply_json(value, now, default_worker);
}
fn apply_stats(&mut self, value: &Value, now: Instant) {
if let Some(num_workers) = u32_field(value, &["num_workers", "workers_live"]) {
self.num_workers = Some(num_workers);
}
if let Some(uptime_ms) = u64_field(value, &["uptime_ms"]) {
self.uptime_ms = Some(uptime_ms);
}
// address -> worker_id mapping; runtime.stats `actors` is [[addr, wid]].
if let Some(actors) = value.get("actors").and_then(Value::as_array) {
for entry in actors {
if let Some(items) = entry.as_array()
&& items.len() >= 2
&& let (Some(address), Some(worker_id)) =
(value_to_string(&items[0]), value_to_u32(&items[1]))
{
self.actors
.entry(address.clone())
.or_insert_with(|| ActorState::new(address))
.worker_id = Some(worker_id);
}
}
}
// Some publishers carry full per-actor detail under `actor_details`.
if let Some(details) = value.get("actor_details").and_then(Value::as_array) {
for actor in details {
self.apply_actor(actor, now, None);
}
}
}
fn push_history(&mut self, now: Instant) {
let totals = self.totals();
if let Some(last) = self.history.back_mut()
&& now.duration_since(last.at) < HISTORY_MIN_INTERVAL
{
last.mailbox_depth = totals.mailbox_depth;
last.msg_per_sec = totals.msg_per_sec;
return;
}
if self.history.len() == HISTORY_CAP {
self.history.pop_front();
}
self.history.push_back(HistorySample {
at: now,
mailbox_depth: totals.mailbox_depth,
msg_per_sec: totals.msg_per_sec,
});
}
fn totals(&self) -> Totals {
let mut totals = Totals::default();
totals.actors = self.actors.len().min(u32::MAX as usize) as u32;
for actor in self.actors.values() {
totals.mailbox_depth = totals.mailbox_depth.saturating_add(actor.mailbox_depth);
totals.msg_per_sec += actor.msg_per_sec;
if actor.poisoned {
totals.poisoned = totals.poisoned.saturating_add(1);
}
}
totals
}
}
#[derive(Clone)]
struct ActorState {
address: String,
name: Option<String>,
actor_type: Option<String>,
message_type: Option<String>,
worker_id: Option<u32>,
mailbox_depth: u32,
mailbox_growth: f64,
messages_processed: u64,
msg_per_sec: f64,
last_msg_type: Option<String>,
poisoned: bool,
message_type_counts: Vec<(String, u64)>,
history: VecDeque<ActorHistorySample>,
last_update: Option<Instant>,
}
impl ActorState {
fn new(address: String) -> Self {
Self {
address,
name: None,
actor_type: None,
message_type: None,
worker_id: None,
mailbox_depth: 0,
mailbox_growth: 0.0,
messages_processed: 0,
msg_per_sec: 0.0,
last_msg_type: None,
poisoned: false,
message_type_counts: Vec::new(),
history: VecDeque::new(),
last_update: None,
}
}
fn apply_json(&mut self, value: &Value, now: Instant, default_worker: Option<u32>) {
let elapsed = self
.last_update
.map(|then| now.duration_since(then).as_secs_f64())
.unwrap_or(0.0);
if let Some(name) = string_field(value, &["name"]).filter(|name| !name.is_empty()) {
self.name = Some(name);
}
if let Some(actor_type) = string_field(value, &["actor_type"]).filter(|t| !t.is_empty()) {
self.actor_type = Some(actor_type);
}
if let Some(message_type) = string_field(value, &["message_type"]).filter(|t| !t.is_empty()) {
self.message_type = Some(message_type);
}
if let Some(worker_id) = u32_field(value, &["worker_id", "worker"]) {
self.worker_id = Some(worker_id);
} else if self.worker_id.is_none() {
self.worker_id = default_worker;
}
assign_u32(&mut self.mailbox_depth, value, &["mailbox_depth", "queued"]);
assign_u64_rate(
&mut self.messages_processed,
&mut self.msg_per_sec,
value,
&["messages_processed", "processed", "messages_handled"],
elapsed,
);
if let Some(last) = string_field(
value,
&["last_msg_type", "last_message", "last_message_type"],
)
.filter(|last| !last.is_empty())
{
self.last_msg_type = Some(last);
}
if let Some(poisoned) = value.get("poisoned").and_then(Value::as_bool) {
self.poisoned = poisoned;
}
if let Some(counts) = parse_message_type_counts(value.get("message_type_counts")) {
self.message_type_counts = counts;
}
self.last_update = Some(now);
self.push_history(now);
self.recompute_growth();
}
fn push_history(&mut self, now: Instant) {
if let Some(last) = self.history.back_mut()
&& now.duration_since(last.at) < HISTORY_MIN_INTERVAL
{
last.mailbox_depth = self.mailbox_depth;
last.msg_per_sec = self.msg_per_sec;
return;
}
if self.history.len() == PER_ACTOR_HISTORY_CAP {
self.history.pop_front();
}
self.history.push_back(ActorHistorySample {
at: now,
mailbox_depth: self.mailbox_depth,
msg_per_sec: self.msg_per_sec,
});
}
/// Mailbox depth/s over the recent window, for trend colouring.
fn recompute_growth(&mut self) {
let n = self.history.len();
if n < 2 {
self.mailbox_growth = 0.0;
return;
}
let start = n - n.min(GROWTH_WINDOW);
let first = &self.history[start];
let last = &self.history[n - 1];
let span = last.at.duration_since(first.at).as_secs_f64();
self.mailbox_growth = if span > 0.0 {
(last.mailbox_depth as f64 - first.mailbox_depth as f64) / span
} else {
0.0
};
}
}
#[derive(Clone, Copy)]
struct HistorySample {
at: Instant,
mailbox_depth: u32,
msg_per_sec: f64,
}
#[derive(Clone, Copy)]
struct ActorHistorySample {
at: Instant,
mailbox_depth: u32,
msg_per_sec: f64,
}
#[derive(Default)]
struct Totals {
actors: u32,
mailbox_depth: u32,
msg_per_sec: f64,
poisoned: u32,
}
#[derive(Serialize)]
struct PanelSnapshot {
runtimes: Vec<RuntimeSnapshot>,
}
#[derive(Serialize)]
struct RuntimeSnapshot {
stream: StreamSnapshot,
live: bool,
last_seen_ms_ago: u64,
summary: SummarySnapshot,
actors: Vec<ActorSnapshot>,
history: Vec<HistorySnapshot>,
}
#[derive(Serialize)]
struct StreamSnapshot {
key: String,
node: String,
life: u64,
}
#[derive(Serialize)]
struct SummarySnapshot {
actors: u32,
msg_per_sec: f64,
mailbox_depth: u32,
poisoned: u32,
uptime_ms: Option<u64>,
num_workers: Option<u32>,
}
#[derive(Serialize)]
struct ActorSnapshot {
address: String,
name: Option<String>,
actor_type: Option<String>,
message_type: Option<String>,
worker_id: Option<u32>,
/// Single derived display state. The frame carries only `poisoned`, so the
/// granularity is poisoned | running until the feed is enriched.
state: &'static str,
mailbox_depth: u32,
mailbox_growth: f64,
messages_processed: u64,
msg_per_sec: f64,
last_msg_type: Option<String>,
poisoned: bool,
message_type_counts: Vec<MessageTypeCountSnapshot>,
history: Vec<ActorHistorySnapshot>,
last_seen_ms_ago: u64,
}
#[derive(Serialize)]
struct MessageTypeCountSnapshot {
message_type: String,
count: u64,
}
#[derive(Serialize)]
struct HistorySnapshot {
ms_ago: u64,
msg_per_sec: f64,
mailbox_depth: u32,
}
#[derive(Serialize)]
struct ActorHistorySnapshot {
ms_ago: u64,
mailbox_depth: u32,
msg_per_sec: f64,
}
impl DashboardView for ActorPanelView {
fn id(&self) -> &'static str {
self.id
}
fn title(&self) -> &'static str {
self.title
}
fn path(&self) -> &'static str {
self.path
}
fn channels(&self) -> &'static [&'static str] {
CHANNELS
}
fn ingest(&self, _stream: &StreamId, _frame: &Frame, event: &FrameEvent) {
let now = Instant::now();
let mut state = self.state.write();
let key = stream_key(&event.stream);
state
.runtimes
.entry(key)
.or_insert_with(|| RuntimeState::new(event.stream.clone(), now))
.update(&event.channel, &event.payload, now);
}
fn snapshot_json(&self) -> Value {
let now = Instant::now();
let snapshot = PanelSnapshot {
runtimes: self
.state
.read()
.runtimes
.values()
.map(|runtime| runtime_snapshot(runtime, now))
.collect(),
};
serde_json::to_value(snapshot).unwrap_or_else(|_| json!({ "runtimes": [] }))
}
fn html(&self) -> Option<&'static str> {
Some(self.html)
}
}
fn runtime_snapshot(runtime: &RuntimeState, now: Instant) -> RuntimeSnapshot {
let totals = runtime.totals();
RuntimeSnapshot {
stream: StreamSnapshot {
key: stream_key(&runtime.stream),
node: runtime.stream.node.clone(),
life: runtime.stream.life,
},
live: now.duration_since(runtime.last_seen) <= LIVE_TTL,
last_seen_ms_ago: now.duration_since(runtime.last_seen).as_millis() as u64,
summary: SummarySnapshot {
actors: totals.actors,
msg_per_sec: totals.msg_per_sec,
mailbox_depth: totals.mailbox_depth,
poisoned: totals.poisoned,
uptime_ms: runtime.uptime_ms,
num_workers: runtime.num_workers,
},
actors: runtime
.actors
.values()
.map(|actor| ActorSnapshot {
address: actor.address.clone(),
name: actor.name.clone(),
actor_type: actor.actor_type.clone(),
message_type: actor.message_type.clone(),
worker_id: actor.worker_id,
state: if actor.poisoned { "poisoned" } else { "running" },
mailbox_depth: actor.mailbox_depth,
mailbox_growth: actor.mailbox_growth,
messages_processed: actor.messages_processed,
msg_per_sec: actor.msg_per_sec,
last_msg_type: actor.last_msg_type.clone(),
poisoned: actor.poisoned,
message_type_counts: actor
.message_type_counts
.iter()
.map(|(message_type, count)| MessageTypeCountSnapshot {
message_type: message_type.clone(),
count: *count,
})
.collect(),
history: actor
.history
.iter()
.map(|sample| ActorHistorySnapshot {
ms_ago: now.duration_since(sample.at).as_millis() as u64,
mailbox_depth: sample.mailbox_depth,
msg_per_sec: sample.msg_per_sec,
})
.collect(),
last_seen_ms_ago: actor
.last_update
.map(|then| now.duration_since(then).as_millis() as u64)
.unwrap_or(0),
})
.collect(),
history: runtime
.history
.iter()
.map(|sample| HistorySnapshot {
ms_ago: now.duration_since(sample.at).as_millis() as u64,
msg_per_sec: sample.msg_per_sec,
mailbox_depth: sample.mailbox_depth,
})
.collect(),
}
}
// --- tolerant JSON helpers (publisher-shape-agnostic readers) ---------------
fn assign_u32(slot: &mut u32, value: &Value, names: &[&str]) {
if let Some(v) = u32_field(value, names) {
*slot = v;
}
}
fn assign_u64_rate(slot: &mut u64, rate: &mut f64, value: &Value, names: &[&str], elapsed: f64) {
if let Some(next) = u64_field(value, names) {
if elapsed > 0.0 && next > *slot {
*rate = (next - *slot) as f64 / elapsed;
} else if next < *slot {
*rate = 0.0;
}
*slot = next;
}
}
fn u32_field(value: &Value, names: &[&str]) -> Option<u32> {
u64_field(value, names).and_then(|v| u32::try_from(v).ok())
}
fn u64_field(value: &Value, names: &[&str]) -> Option<u64> {
names
.iter()
.find_map(|name| value.get(*name).and_then(value_to_u64))
}
fn string_field(value: &Value, names: &[&str]) -> Option<String> {
names
.iter()
.find_map(|name| value.get(*name).and_then(value_to_string))
}
fn value_to_u32(value: &Value) -> Option<u32> {
value_to_u64(value).and_then(|v| u32::try_from(v).ok())
}
fn value_to_u64(value: &Value) -> Option<u64> {
value
.as_u64()
.or_else(|| value.as_str().and_then(|s| s.parse::<u64>().ok()))
}
fn value_to_string(value: &Value) -> Option<String> {
value.as_str().map(ToOwned::to_owned).or_else(|| {
if value.is_null() {
None
} else {
Some(value.to_string())
}
})
}
/// Accepts pair form `["Type", N]`, object form `{"ty": "Type", "count": N}`,
/// and map form `{"Type": N}`.
fn parse_message_type_counts(value: Option<&Value>) -> Option<Vec<(String, u64)>> {
let value = value?;
if let Some(items) = value.as_array() {
let mut out = Vec::new();
for item in items {
if let Some(pair) = item.as_array()
&& pair.len() >= 2
&& let (Some(name), Some(count)) =
(value_to_string(&pair[0]), value_to_u64(&pair[1]))
{
out.push((name, count));
continue;
}
if let Some(name) = string_field(item, &["ty", "message_type", "type", "name"])
&& let Some(count) = u64_field(item, &["count"])
{
out.push((name, count));
}
}
return Some(out);
}
if let Some(map) = value.as_object() {
let mut out: Vec<(String, u64)> = map
.iter()
.filter_map(|(name, count)| value_to_u64(count).map(|count| (name.clone(), count)))
.collect();
out.sort_by(|a, b| b.1.cmp(&a.1));
return Some(out);
}
None
}
fn stream_key(stream: &StreamEvent) -> String {
format!("{}#{}", stream.node, stream.life)
}

View file

@ -2,12 +2,10 @@ use std::sync::Arc;
use crate::view::DashboardView; use crate::view::DashboardView;
mod actor_view;
mod worker_page; mod worker_page;
mod worker_view; mod worker_view;
pub use worker_view::SwactorWorkerView; pub use worker_view::SwactorWorkerView;
pub use actor_view::ActorPanelView;
pub const RUNTIME_STATS: &str = "runtime.stats"; pub const RUNTIME_STATS: &str = "runtime.stats";
pub const RUNTIME_WORKERS: &str = "runtime.workers"; pub const RUNTIME_WORKERS: &str = "runtime.workers";
@ -16,13 +14,3 @@ pub const RUNTIME_ACTORS: &str = "runtime.actors";
pub fn worker_view() -> Arc<dyn DashboardView> { pub fn worker_view() -> Arc<dyn DashboardView> {
Arc::new(SwactorWorkerView::default()) Arc::new(SwactorWorkerView::default())
} }
/// Built-in actor overview + roster view (`/view/swactor/actor-overview`).
pub fn actor_overview_view() -> Arc<dyn DashboardView> {
Arc::new(ActorPanelView::overview())
}
/// Built-in actor dossier view (`/view/swactor/actor-dossier`).
pub fn actor_dossier_view() -> Arc<dyn DashboardView> {
Arc::new(ActorPanelView::dossier())
}

View file

@ -2,7 +2,6 @@
name = "data-plane" name = "data-plane"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "AGPL-3.0-only"
publish = false publish = false
[dependencies] [dependencies]

View file

@ -0,0 +1,523 @@
# Data Plane Actor Architecture Specification
**Status:** implemented architecture contract for the `data-plane` crate.
This document describes the behavioral boundary between reusable data-plane actors
and MVP-specific orchestration/runtime code. It is intentionally architectural: it
names responsibilities, actor roles, message families, and ownership boundaries
without prescribing file layout or migration steps.
---
## 1. Purpose
`data-plane` owns the behavior required to move model objects between stages and
between a node process and its local GPU worker process.
The crate defines the actor protocol for:
- provisioning logical data edges;
- distinguishing network/wire edges from node-local IPC rings;
- leasing, installing, readying, faulting, stopping, quiescing, and releasing
local rings;
- binding logical edges to transport endpoints and local worker rings;
- parsing, validating, sequencing, loading, producing, and reporting objects;
- gating readiness and object visibility on the correct lifecycle transitions;
- translating low-level arena, transport, and worker observations into coarse
data-plane outcomes.
`mvp-system` uses `data-plane` as a reusable actor subsystem. It provides run
intent, concrete runtime actor addresses, and MVP-specific report sinks. It does
not own the fine-grained data-plane state machine.
---
## 2. Core boundary
Swactor actors carry control, lifecycle, and identity messages. Payload bytes do
not move through actor mailboxes.
Payload bytes move through:
- arena-backed shared-memory rings for node-local IPC;
- transport byte streams for node-to-node data edges;
- GPU-worker-owned device allocations for compute-ready objects.
The data-plane actors decide when these byte paths are established, readable,
writable, faulted, stopped, and safe to release. Runtime adapter actors execute
concrete effects and report observations back.
---
## 3. Edge and ring model
### 3.1 Wire edge
A wire edge is a logical run-plan connection between a producer endpoint and a
consumer endpoint.
It carries:
- run identity;
- edge identity;
- producer and consumer node identity;
- edge kind, such as token input, activation, or token output;
- object contract;
- transport contract;
- optional remote endpoint and remote actor identity.
A wire edge answers: "which logical data stream connects these stage endpoints?"
It does not answer: "which local arena offset or worker-process ring is being
used on this node?"
### 3.2 Local IPC ring
A local IPC ring is a node-local buffer used by a Rust node process and its owned
GPU worker process.
It carries:
- ring identity allocated by the local arena manager;
- arena layout and capacity;
- worker-process generation;
- local role port, such as input or output;
- direction relative to the worker process;
- object contract installed into the worker;
- quiescence and release state.
A local IPC ring answers: "how does this node exchange bytes with its local
worker process for a specific edge endpoint?"
It does not answer: "which remote node or distributed route owns the other side
of the logical edge?"
### 3.3 Binding
A wire edge endpoint may bind to zero or more local resources depending on its
role:
```text
inbound wire edge endpoint
-> recv transport endpoint
-> local worker ingress ring
-> device object handle
outbound wire edge endpoint
-> local worker egress ring
-> send transport endpoint
local-only edge endpoint
-> local producer/consumer binding
-> optional worker ring
```
The binding is owned by data-plane state. MVP code may observe the binding only
through coarse reports such as edge ready, object loaded, object produced, edge
faulted, and edge stopped.
---
## 4. Actor topology
The target topology is actor-oriented.
### 4.1 Data-plane node actor
One data-plane node actor owns the data-plane state for one local node within one
active run.
It owns:
- local node identity;
- run-scoped edge table;
- mapping from wire edge endpoints to local rings;
- mapping from ring ids to edge endpoints;
- object sequence state;
- device-handle visibility state;
- data-plane child actor addresses;
- MVP report sink addresses.
It receives provisioning intent from MVP code and observations from runtime
adapters. It emits actor messages to arena, worker, transport, and MVP report
sinks.
### 4.2 Wire edge actor
A wire edge actor owns the lifecycle of one logical edge endpoint on the local
node.
It owns:
- provisioning state;
- transport establishment state;
- send/receive pump readiness;
- stream faults;
- logical edge readiness;
- stop and fault propagation for that edge endpoint.
It does not own worker-process state or arena layout details except through a
binding supplied by the data-plane node actor or ring actor.
### 4.3 Local worker ring actor
A local worker ring actor owns one local IPC ring lifecycle.
It owns:
- arena lease request and result;
- worker ring installation;
- worker ring readable/writable notifications;
- ring fault and quiescence observations;
- release proof collection;
- arena lease release.
It does not own remote endpoint routing. It can be bound to a wire edge endpoint
by edge id, but the ring lifecycle remains local.
### 4.4 GPU worker control adapter
The GPU worker control adapter is the actor-facing boundary to the owned Python
worker process.
It owns or fronts:
- worker process generation;
- command serialization to the worker;
- stdout/stderr event parsing;
- device handle generation checks;
- worker stop/crash/restart observations.
The data plane treats this as an actor endpoint. Worker-process JSON and Python
helper details are not exposed to MVP stage logic.
### 4.5 Transport adapter actors
Transport adapter actors own concrete wire byte movement.
They own or front:
- accepted edge streams;
- outbound edge streams;
- edge preamble validation;
- byte read/write readiness;
- transport-specific stream faults;
- pump stop observations.
The data plane treats transport events as observations on a wire edge. Transport
actors do not decide stage readiness or object admission.
### 4.6 MVP report sink
The MVP report sink receives coarse data-plane outcomes and maps them to
MVP-specific control messages.
Examples:
- inbound edge ready;
- outbound edge ready;
- object loaded for stage execution;
- object produced for downstream transport;
- edge faulted;
- local edges stopped.
The sink does not inspect ring cursors, arena leases, worker generations, or
transport pump internals.
---
## 5. Actor API surface
Concrete Rust names are schematic. The contract is the message shape and
ownership boundary.
### 5.1 Provisioning input
MVP sends provisioning intent to the data-plane node actor:
```text
ProvisionDataPlaneRun {
run_id,
local_node_id,
arena_actor,
worker_actor,
transport_actor,
report_sink,
}
ProvisionWireEdgeEndpoint {
run_id,
edge_id,
direction,
edge_kind,
local_role_port,
local_node_id,
peer_node_id,
peer_endpoint,
object_spec,
transport_spec,
local_ring_spec,
}
```
`direction` is relative to the local node's stage role: inbound means the local
stage consumes objects from the edge; outbound means the local stage produces
objects to the edge.
Provisioning is declarative. MVP describes the intended edge endpoint and the
actors available to execute effects. It does not prescribe lease/install/driver
ordering.
### 5.2 Runtime observations
Runtime adapters report observations back to data-plane actors:
```text
ArenaRingLeased
ArenaRingLeaseRejected
ArenaRingReleased
ArenaRingReleaseRejected
WorkerReady
WorkerRingInstalled
WorkerRingFaulted
WorkerRingQuiesced
WorkerRingReadable
WorkerRingWritable
WorkerObjectLoaded
WorkerObjectProduced
WorkerObjectFailed
WorkerStopped
WorkerFaulted
TransportEdgeReady
TransportBytesReceived
TransportBytesSent
TransportStreamClosed
TransportStreamFaulted
TransportPumpStopped
```
Observations are facts, not commands. The data plane decides the next state and
any follow-up messages.
### 5.3 Data-plane effects
The data plane sends effect requests to runtime adapter actors:
```text
LeaseArenaRing
CancelArenaRingLease
ReleaseArenaRingLease
InstallWorkerRing
UninstallWorkerRing
NotifyWorkerRingReadable
NotifyWorkerRingWritable
LoadObjectFromWorkerRing
ExecuteWorkerStep
ReleaseWorkerDeviceObject
EstablishWireSend
EstablishWireRecv
WriteWireObject
StopWirePump
```
Effects are actor messages. The receiving adapter owns the concrete mechanism:
memfd/mmap, JSON stdin/stdout, process supervision, iroh streams, or test doubles.
### 5.4 Data-plane reports
The data plane reports only stable semantic outcomes to MVP:
```text
InboundEdgeReady { edge_id }
OutboundEdgeReady { edge_id }
ObjectLoaded { edge_id, object_id, sequence, device_handle }
ObjectProduced { edge_id, object_id, sequence, extent }
EdgeFaulted { edge_id, reason }
EdgeStopped { edge_id }
LocalEdgesStopped { run_id }
WorkerDataPlaneFaulted { reason }
```
Reports are the only data-plane messages MVP stage/orchestrator actors should
need for normal stage progression.
---
## 6. Behavior owned by data-plane
### 6.1 Edge establishment
For each provisioned edge endpoint, data-plane actors own the establishment
sequence.
Inbound endpoint:
```text
provision endpoint
-> lease local ingress ring
-> install ring into worker input port
-> establish receive transport if the edge is remote
-> report inbound edge ready
```
Outbound endpoint:
```text
provision endpoint
-> lease local egress ring when worker output is required
-> install ring into worker output port
-> establish send transport if the edge is remote
-> report outbound edge ready
```
Readiness is reported only after every required local and wire resource for that
endpoint is ready. A local-only endpoint may omit transport establishment. A
wire-only endpoint may omit worker-ring establishment when it terminates outside
the local GPU worker.
### 6.2 Object ingress
For inbound data, data-plane actors own object admission.
The data plane:
- associates incoming bytes with the correct wire edge and stream;
- validates object framing and object spec constraints;
- preserves sequence ordering required by the edge contract;
- writes or exposes the object through the local ingress ring;
- asks the worker to load the object to device;
- waits for a valid worker object-loaded observation;
- reports object loaded to MVP only after the device handle is current and the
logical object is complete.
MVP does not parse object headers, track ingress buffers, reload cursors, or gate
object-loaded visibility.
### 6.3 Object egress
For outbound data, data-plane actors own object production and forwarding.
The data plane:
- receives compute/output observations from the worker;
- binds produced objects to the correct outbound edge and sequence;
- validates object extent and object contract;
- publishes readable/writable state to the worker and transport actors;
- forwards complete object records on the wire when the edge is remote;
- reports object produced or step-visible outcomes to MVP at semantic
boundaries, not cursor boundaries.
MVP does not decide when a local output ring is readable, when a transport stream
should consume it, or when a produced object is safe to expose downstream.
### 6.4 Faults
The data plane owns data movement fault classification and propagation.
Fault sources include:
- arena lease rejection or release rejection;
- worker ring installation failure;
- worker ring fault;
- malformed object framing;
- sequence violation;
- worker object load/produce failure;
- transport stream read/write/protocol failure;
- pump stop before quiescence;
- stale worker generation or device handle.
A fault on one edge endpoint must not silently corrupt another endpoint. The data
plane maps local faults to edge-scoped or worker-scoped reports, starts the
required stop/quiescence path, and emits the appropriate MVP report.
### 6.5 Stop, quiescence, and release
The data plane owns teardown ordering for local resources.
For a bound edge/ring pair, stop requires:
```text
stop transport pump if present
-> uninstall or quiesce worker ring if installed
-> prove no local reader/writer still uses the ring
-> release arena lease
-> report edge stopped
```
Arena release must be gated by quiescence proof. MVP may request run or edge
stop, but it does not supply low-level release proof or decide when a ring is
safe to release.
---
## 7. MVP-system utilization
`mvp-system` remains responsible for MVP orchestration and stage semantics.
It owns:
- run planning and edge assignment;
- stage provisioning authority;
- membership/readiness gates outside the data plane;
- weight loading and role configuration intent;
- stage controller behavior;
- prompt injection and token consumption;
- mapping data-plane reports to MVP lifecycle messages;
- selecting concrete runtime adapters for arena, worker process, and transport.
For data movement, MVP code acts as a client:
1. Spawn or obtain actor addresses for the data-plane node actor and required
runtime adapters.
2. Send run and edge provisioning intent to the data-plane node actor.
3. Forward runtime observations from concrete adapter actors when those adapters
are MVP-owned.
4. Receive coarse data-plane reports.
5. Translate those reports into stage-controller or orchestrator messages.
MVP must not rely on private edge states such as waiting-for-lease,
waiting-for-worker-ring, waiting-for-driver, pump-stopped, ring-quiesced, or
release-ready. Those are data-plane implementation states.
---
## 8. Required invariants
- `EdgeId` names a logical run-plan edge, not a local ring allocation.
- `RingId` names a local arena-backed IPC ring, not a distributed edge.
- A ring may be bound to an edge endpoint, but the identifiers are not
interchangeable.
- Actor messages carry control and identities, not tensor payload bytes.
- Object-loaded reports are emitted only for complete, validated objects with
current-generation device handles.
- Edge-ready reports are emitted only after required wire and local IPC resources
are ready.
- Arena lease release is gated by local quiescence proof.
- Worker process generation is part of every device-handle validity decision.
- Transport faults and worker faults are classified by data-plane before they
become MVP reports.
- MVP stage logic observes semantic outcomes, never ring cursor mechanics.
---
## 9. Non-goals
`data-plane` does not own:
- global run planning;
- placement optimization;
- model layer assignment;
- weight download or weight loading semantics;
- prompt tokenization or output token policy;
- membership convergence;
- provider provisioning;
- concrete iroh endpoint construction;
- concrete Python helper implementation.
The crate defines reusable actor protocols and data-movement behavior. Concrete
runtime adapters may live beside MVP code, inside reusable support crates, or in
tests, as long as they satisfy the actor contracts above.

View file

@ -2,7 +2,6 @@
name = "datastream" name = "datastream"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "AGPL-3.0-only"
[dependencies] [dependencies]
swactor = { path = "../..", features = ["serde", "transport"] } swactor = { path = "../..", features = ["serde", "transport"] }

View file

@ -1,8 +1,9 @@
# The Datastream — Specification # The Datastream — Specification
Id: 7 > **Status.** Current-state specification for the `datastream` crate. This keeps
Last modified: > the original outline, but updates the model to the implementation that now
Last reviewed: > exists: stream-local numeric channel ids, a channel catalog, broadcast endpoint
> subscriptions, and catalog-aware transport events.
--- ---

View file

@ -758,11 +758,9 @@ impl<'a> RuntimeActorStatsRecord<'a> {
actors: snapshots actors: snapshots
.iter() .iter()
.map(|snapshot| RuntimeActorSnapshotRecord { .map(|snapshot| RuntimeActorSnapshotRecord {
address: snapshot.address.to_full_hex(), address: snapshot.address.to_string(),
mailbox_depth: snapshot.mailbox_depth, mailbox_depth: snapshot.mailbox_depth,
last_msg_type: snapshot.last_msg_type, last_msg_type: snapshot.last_msg_type,
actor_type: snapshot.actor_type,
message_type: snapshot.message_type,
messages_processed: snapshot.messages_processed, messages_processed: snapshot.messages_processed,
poisoned: snapshot.poisoned, poisoned: snapshot.poisoned,
message_type_counts: snapshot message_type_counts: snapshot
@ -781,8 +779,6 @@ struct RuntimeActorSnapshotRecord<'a> {
address: String, address: String,
mailbox_depth: usize, mailbox_depth: usize,
last_msg_type: Option<&'static str>, last_msg_type: Option<&'static str>,
actor_type: Option<&'static str>,
message_type: Option<&'static str>,
messages_processed: u64, messages_processed: u64,
poisoned: bool, poisoned: bool,
message_type_counts: Vec<RuntimeMessageTypeCount<'a>>, message_type_counts: Vec<RuntimeMessageTypeCount<'a>>,

View file

@ -300,8 +300,6 @@ fn stats_hook_adapter_submits_worker_snapshot_json() {
address: actor, address: actor,
mailbox_depth: 3, mailbox_depth: 3,
last_msg_type: Some("Ping"), last_msg_type: Some("Ping"),
actor_type: Some("TestActor"),
message_type: Some("Ping"),
messages_processed: 5, messages_processed: 5,
poisoned: false, poisoned: false,
message_type_counts: vec![("Ping", 5)], message_type_counts: vec![("Ping", 5)],
@ -315,13 +313,11 @@ fn stats_hook_adapter_submits_worker_snapshot_json() {
assert_eq!(delivery.channel.channel, runtime); assert_eq!(delivery.channel.channel, runtime);
let json: Value = serde_json::from_slice(&delivery.payload).unwrap(); let json: Value = serde_json::from_slice(&delivery.payload).unwrap();
assert_eq!(json["worker_id"], 2); assert_eq!(json["worker_id"], 2);
assert_eq!(json["actors"][0]["address"], actor.to_full_hex()); assert_eq!(json["actors"][0]["address"], actor.to_string());
assert_eq!(json["actors"][0]["mailbox_depth"], 3); assert_eq!(json["actors"][0]["mailbox_depth"], 3);
assert_eq!(json["actors"][0]["last_msg_type"], "Ping"); assert_eq!(json["actors"][0]["last_msg_type"], "Ping");
assert_eq!(json["actors"][0]["messages_processed"], 5); assert_eq!(json["actors"][0]["messages_processed"], 5);
assert_eq!(json["actors"][0]["message_type_counts"][0]["ty"], "Ping"); assert_eq!(json["actors"][0]["message_type_counts"][0]["ty"], "Ping");
assert_eq!(json["actors"][0]["actor_type"], "TestActor");
assert_eq!(json["actors"][0]["message_type"], "Ping");
} }
fn positions(events: &[DatastreamEvent]) -> Vec<u64> { fn positions(events: &[DatastreamEvent]) -> Vec<u64> {

View file

@ -2,7 +2,6 @@
name = "distribution" name = "distribution"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "AGPL-3.0-only"
[features] [features]
default = [] default = []

View file

@ -2,7 +2,6 @@
name = "iroh-driver" name = "iroh-driver"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "AGPL-3.0-only"
[lib] [lib]
name = "iroh_driver" name = "iroh_driver"
@ -20,8 +19,9 @@ iroh = "0.98"
# `test-utils` exposes `CaRootsConfig::insecure_skip_verify()` so clients can # `test-utils` exposes `CaRootsConfig::insecure_skip_verify()` so clients can
# trust operator-controlled custom relays with self-signed QAD certs. # trust operator-controlled custom relays with self-signed QAD certs.
iroh-relay = { version = "0.98", features = ["test-utils"] } iroh-relay = { version = "0.98", features = ["test-utils"] }
tokio.workspace = true tokio = { version = "1", features = ["rt-multi-thread", "time"] }
parking_lot = "0.12" parking_lot = "0.12"
[dev-dependencies] [dev-dependencies]
iroh-relay = { version = "0.98", features = ["server", "test-utils"] } iroh-relay = { version = "0.98", features = ["server", "test-utils"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time"] }

View file

@ -1,8 +1,6 @@
# Iroh Driver Fixed Specification # Iroh Driver Fixed Specification
Id: 5 **Status:** draft current-state specification for `iroh-driver`.
Last modified:
Last reviewed:
> Review checkpoint: reviewed through Section 3.2; resume with Section 3.3 Accepted Connection Output. > Review checkpoint: reviewed through Section 3.2; resume with Section 3.3 Accepted Connection Output.

View file

@ -0,0 +1,47 @@
[package]
name = "mvp-system"
version = "0.1.0"
edition = "2024"
publish = false
autobins = false
[features]
default = []
dashboard = []
[dependencies]
datastream = { path = "../datastream" }
data-plane = { path = "../data-plane" }
provisioning = { path = "../provisioning" }
dashboard = { path = "../dashboard" }
serde_json = "1"
serde = { version = "1", features = ["derive"] }
swactor = { path = "../..", features = ["serde", "transport"] }
swactor-transport = { path = "../transport" }
swactor-process = { path = "../process" }
distribution = { path = "../distribution" }
iroh-driver = { path = "../iroh-driver" }
iroh = "0.98"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "io-util", "sync", "time", "net", "signal"] }
swactor-vastai = { path = "../../tools/vastai" }
parking_lot = "0.12"
blake3 = "1"
toml = "0.8"
ureq = "2"
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
signal-hook = "0.3"
[[bin]]
name = "mvp-worker-node"
path = "src/bin/worker_node.rs"
[[bin]]
name = "mvp-orchestrator"
path = "src/bin/orchestrator.rs"
[[bin]]
name = "mvp-chat"
path = "src/bin/mvp_chat.rs"

View file

@ -1,16 +1,14 @@
# Myelin System Specification # MVP System Specification
***STALE! FOR HISTORICAL REFERENCE ONLY*** ***STALE! FOR HISTORICAL REFERENCE ONLY***
Id: 9 **Status:** draft consolidated system specification.
Last modified:
Last reviewed:
This document is the single Myelin reference for the swactor GGUF pipeline system. This document is the single MVP reference for the swactor GGUF pipeline system.
It folds the system behavior previously split across the orchestration, It folds the system behavior previously split across the orchestration,
ring/backpressure, and GPU-worker drafts into one end-to-end specification. ring/backpressure, and GPU-worker drafts into one end-to-end specification.
The focused root specs may remain as design history while this document is being The focused root specs may remain as design history while this document is being
stabilized. For the Myelin drafting process, this document is the place where the stabilized. For the MVP drafting process, this document is the place where the
complete system, its components, guarantees, and systemic behavior are described complete system, its components, guarantees, and systemic behavior are described
together. together.
@ -18,7 +16,7 @@ together.
## 1. Purpose ## 1. Purpose
The Myelin is a trusted distributed ML runtime for one linear GGUF inference The MVP is a trusted distributed ML runtime for one linear GGUF inference
pipeline over a rented pool of GPU nodes. pipeline over a rented pool of GPU nodes.
The first deployment shape is: The first deployment shape is:
@ -38,7 +36,7 @@ operator provisions rented GPU nodes
``` ```
The document is intentionally about the first complete feature, not a broad The document is intentionally about the first complete feature, not a broad
graph engine. Later features can generalize from this system after the Myelin graph engine. Later features can generalize from this system after the MVP
behavior is stable. behavior is stable.
--- ---
@ -102,7 +100,7 @@ no actor-level credit protocol, RTS/CTS exchange, or per-range acknowledgement.
One persistent iroh uni-stream carries all objects for one edge. The stream One persistent iroh uni-stream carries all objects for one edge. The stream
starts with an `edge_id` preamble and then object records. starts with an `edge_id` preamble and then object records.
The Myelin permits transfer and device upload/download to overlap with network The MVP permits transfer and device upload/download to overlap with network
streaming. It does not permit compute to observe a logical object until the streaming. It does not permit compute to observe a logical object until the
whole object has loaded. whole object has loaded.
@ -148,7 +146,7 @@ its active run plan.
Other stages cannot redirect a node to a new peer, edge id, or layer range. Other stages cannot redirect a node to a new peer, edge id, or layer range.
Resource inventory is known to the orchestrator before placement in the Myelin. A Resource inventory is known to the orchestrator before placement in the MVP. A
node may report boot health and readiness, but there is no distributed node may report boot health and readiness, but there is no distributed
capability negotiation protocol after boot. capability negotiation protocol after boot.
@ -182,7 +180,7 @@ Python/tinygrad worker process
The orchestrator may run in its own container or on one of the provisioned GPU The orchestrator may run in its own container or on one of the provisioned GPU
machines. It is a control participant and token endpoint. It does not run model machines. It is a control participant and token endpoint. It does not run model
compute for the Myelin. compute for the MVP.
When the orchestrator participates in token edges over the data plane, it has a When the orchestrator participates in token edges over the data plane, it has a
stable `node_id`, an iroh endpoint, and local token edge endpoints like any stable `node_id`, an iroh endpoint, and local token edge endpoints like any
@ -243,7 +241,7 @@ restart invalidates all prior handles, roles, rings, and in-flight steps.
## 7. System Lifecycle ## 7. System Lifecycle
The complete Myelin lifecycle is: The complete MVP lifecycle is:
```text ```text
Deployment Deployment
@ -318,7 +316,7 @@ No run edge should be provisioned to that node.
The orchestrator starts run planning only after the intended candidate pool is The orchestrator starts run planning only after the intended candidate pool is
ready. ready.
For the Myelin, `PoolReady` means: For the MVP, `PoolReady` means:
- every node in the intended candidate pool is known to the orchestrator - every node in the intended candidate pool is known to the orchestrator
- every candidate node is live in the SWIM membership view - every candidate node is live in the SWIM membership view
@ -336,14 +334,14 @@ membership/liveness input to the orchestrator, not a distributed graph protocol.
If pool readiness is lost before a `RunPlan` is committed, the orchestrator keeps If pool readiness is lost before a `RunPlan` is committed, the orchestrator keeps
waiting or aborts before provisioning according to local policy. waiting or aborts before provisioning according to local policy.
If pool readiness is lost after provisioning begins, the run faults. The Myelin does If pool readiness is lost after provisioning begins, the run faults. The MVP does
not re-place an active run after a node disappears. not re-place an active run after a node disappears.
--- ---
## 10. GGUF Pipeline Workload ## 10. GGUF Pipeline Workload
The Myelin workload is one linear pipeline: The MVP workload is one linear pipeline:
```text ```text
orchestrator --tokens--> stage 0 --activations--> stage 1 --activations--> orchestrator --tokens--> stage 0 --activations--> stage 1 --activations-->
@ -647,7 +645,7 @@ Stopped
terminal for this run terminal for this run
``` ```
The Myelin allows one active `ExecuteStep` per stage. The MVP allows one active `ExecuteStep` per stage.
--- ---
@ -754,7 +752,7 @@ ObjectHeader
payload bytes, exactly header.extent bytes payload bytes, exactly header.extent bytes
``` ```
The Myelin header is fixed-size: The MVP header is fixed-size:
```rust ```rust
struct ObjectHeader { struct ObjectHeader {
@ -842,7 +840,7 @@ struct RingLayout {
pointers from its own arena mapping base plus arena offsets. pointers from its own arena mapping base plus arena offsets.
The ring header and data region must be aligned for cross-process atomics and The ring header and data region must be aligned for cross-process atomics and
backend copy requirements. Host pinning is optional for the Myelin, but if a range backend copy requirements. Host pinning is optional for the MVP, but if a range
is pinned, it cannot be unpinned or returned to the free list until ring is pinned, it cannot be unpinned or returned to the free list until ring
quiescence. quiescence.
@ -1103,7 +1101,7 @@ The worker parses object records from the ring.
The stream is persistent for the edge. It is not opened per object. The stream is persistent for the edge. It is not opened per object.
QUIC owns reliability and stream ordering. The Myelin does not add app-level QUIC owns reliability and stream ordering. The MVP does not add app-level
fragment hashes, striping, resume, or per-object streams. fragment hashes, striping, resume, or per-object streams.
--- ---
@ -2012,7 +2010,7 @@ Run fault sources include:
Teardown failures are recorded in teardown outcome/status. They do not rewrite Teardown failures are recorded in teardown outcome/status. They do not rewrite
a completed inference into a faulted run outcome. a completed inference into a faulted run outcome.
Myelin recovery policy is fail-stop at the run level. The orchestrator records the MVP recovery policy is fail-stop at the run level. The orchestrator records the
first run-level failure reason, stops injecting objects, and begins teardown. It first run-level failure reason, stops injecting objects, and begins teardown. It
does not re-place the run. does not re-place the run.
@ -2324,7 +2322,7 @@ stable.
--- ---
## 42. Canonical Myelin Scenario ## 42. Canonical MVP Scenario
The first end-to-end scenario is one "hello world" prompt over a rented GPU The first end-to-end scenario is one "hello world" prompt over a rented GPU
pool: pool:

View file

@ -0,0 +1,3 @@
fn main() -> std::process::ExitCode {
mvp_system::run_chat_from_args(std::env::args().skip(1))
}

View file

@ -1,10 +1,10 @@
use std::process::ExitCode; use std::process::ExitCode;
fn main() -> ExitCode { fn main() -> ExitCode {
match myelin::run_orchestrator_from_args(std::env::args().skip(1)) { match mvp_system::run_orchestrator_from_args(std::env::args().skip(1)) {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(error) => { Err(error) => {
eprintln!("myelin-orchestrator: {error}"); eprintln!("mvp-orchestrator: {error}");
ExitCode::from(1) ExitCode::from(1)
} }
} }

View file

@ -1,5 +1,5 @@
use std::process::ExitCode; use std::process::ExitCode;
fn main() -> ExitCode { fn main() -> ExitCode {
myelin::run_worker_node_from_env() mvp_system::run_worker_node_from_env()
} }

View file

@ -0,0 +1,4 @@
//! MVP operator chat wrapper public surface.
mod node_image;
pub(super) mod runtime;

View file

@ -8,19 +8,19 @@ use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
const NODE_IMAGE_CONTENT_INPUTS: &[&str] = &[ const NODE_IMAGE_CONTENT_INPUTS: &[&str] = &[
"apps/myelin/node-image/Dockerfile", "apps/mvp-node/Dockerfile",
"apps/myelin/node-image/tinygrad_worker.py", "apps/mvp-node/tinygrad_worker.py",
]; ];
const BASE_IMAGE_SOURCE_INPUTS: &[&str] = &[ const BASE_IMAGE_SOURCE_INPUTS: &[&str] = &[
"apps/myelin/node-image/Dockerfile.base", "apps/mvp-node/Dockerfile.base",
"apps/myelin/node-image/myelin_entrypoint.sh", "apps/mvp-node/mvp_entrypoint.sh",
]; ];
const NODE_IMAGE_TAG_LABEL: &str = "org.swactor.myelin.node-image-tag"; const NODE_IMAGE_TAG_LABEL: &str = "org.swactor.mvp.node-image-tag";
const NODE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.myelin.node.source-hash"; const NODE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.mvp.node.source-hash";
const NODE_IMAGE_WORKER_HASH_LABEL: &str = "org.swactor.myelin.node.worker-hash"; const NODE_IMAGE_WORKER_HASH_LABEL: &str = "org.swactor.mvp.node.worker-hash";
const NODE_IMAGE_BASE_HASH_LABEL: &str = "org.swactor.myelin.node.base-hash"; const NODE_IMAGE_BASE_HASH_LABEL: &str = "org.swactor.mvp.node.base-hash";
const BASE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.myelin.base.source-hash"; const BASE_IMAGE_SOURCE_HASH_LABEL: &str = "org.swactor.mvp.base.source-hash";
pub(super) struct NodeImageRequest { pub(super) struct NodeImageRequest {
pub(super) requested_image: String, pub(super) requested_image: String,
@ -110,11 +110,11 @@ fn prepare_node_image_inner(
"build", "build",
"--quiet", "--quiet",
"-p", "-p",
"myelin", "mvp-system",
"--bin", "--bin",
"myelin-worker", "mvp-worker-node",
], ],
"build myelin-worker", "build mvp-worker-node",
None, None,
progress, progress,
)?; )?;
@ -128,7 +128,7 @@ fn prepare_node_image_inner(
&root, &root,
vec![relative_path( vec![relative_path(
&root, &root,
&root.join("apps/myelin/node-image/tinygrad_worker.py"), &root.join("apps/mvp-node/tinygrad_worker.py"),
)?], )?],
&[], &[],
)?; )?;
@ -176,14 +176,14 @@ fn prepare_node_image_inner(
&[ &[
"build", "build",
"-f", "-f",
"apps/myelin/node-image/Dockerfile.base", "apps/mvp-node/Dockerfile.base",
"--label", "--label",
base_source_hash_label.as_str(), base_source_hash_label.as_str(),
"-t", "-t",
request.base_image.as_str(), request.base_image.as_str(),
".", ".",
], ],
"build myelin node base image", "build mvp node base image",
Some(&request.base_image), Some(&request.base_image),
progress, progress,
)?; )?;
@ -200,11 +200,11 @@ fn prepare_node_image_inner(
let mut build_args = vec![ let mut build_args = vec![
"build".to_owned(), "build".to_owned(),
"-f".to_owned(), "-f".to_owned(),
"apps/myelin/node-image/Dockerfile".to_owned(), "apps/mvp-node/Dockerfile".to_owned(),
"--build-arg".to_owned(), "--build-arg".to_owned(),
format!("BASE_IMAGE={}", request.base_image), format!("BASE_IMAGE={}", request.base_image),
"--build-arg".to_owned(), "--build-arg".to_owned(),
format!("MYELIN_NODE_BIN={node_bin}"), format!("MVP_NODE_BIN={node_bin}"),
]; ];
for (key, value) in &expected_node_labels { for (key, value) in &expected_node_labels {
build_args.push("--label".to_owned()); build_args.push("--label".to_owned());
@ -215,7 +215,7 @@ fn prepare_node_image_inner(
&root, &root,
"docker", "docker",
&build_args.iter().map(String::as_str).collect::<Vec<_>>(), &build_args.iter().map(String::as_str).collect::<Vec<_>>(),
"build myelin node image", "build mvp node image",
Some(&image_ref), Some(&image_ref),
progress, progress,
)?; )?;
@ -437,7 +437,7 @@ fn ensure_aliases_local(
root, root,
"docker", "docker",
&["tag", source_ref, &alias], &["tag", source_ref, &alias],
"tag myelin node image", "tag mvp node image",
Some(&alias), Some(&alias),
progress, progress,
)?; )?;
@ -461,7 +461,7 @@ fn ensure_aliases_for_remote(
root, root,
"docker", "docker",
&["pull", source_ref], &["pull", source_ref],
"pull myelin node image", "pull mvp node image",
Some(source_ref), Some(source_ref),
progress, progress,
)?; )?;
@ -489,7 +489,7 @@ fn push_image(
root, root,
"docker", "docker",
&["push", image_ref], &["push", image_ref],
"push myelin node image", "push mvp node image",
Some(image_ref), Some(image_ref),
progress, progress,
) )
@ -509,7 +509,7 @@ fn docker_image_labels_match(
} }
fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) { fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
let prune_enabled = std::env::var("MYELIN_NODE_IMAGE_PRUNE") let prune_enabled = std::env::var("MVP_NODE_IMAGE_PRUNE")
.map(|value| { .map(|value| {
let value = value.trim().to_ascii_lowercase(); let value = value.trim().to_ascii_lowercase();
!matches!(value.as_str(), "0" | "false" | "no" | "off") !matches!(value.as_str(), "0" | "false" | "no" | "off")
@ -522,12 +522,12 @@ fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
let tags = match docker_image_tags(root, &image.repository) { let tags = match docker_image_tags(root, &image.repository) {
Ok(tags) => tags, Ok(tags) => tags,
Err(error) => { Err(error) => {
eprintln!("myelin-node-image: prune old dirty images skipped: {error}"); eprintln!("mvp-node-image: prune old dirty images skipped: {error}");
return; return;
} }
}; };
let keep_old = std::env::var("MYELIN_NODE_IMAGE_PRUNE_KEEP") let keep_old = std::env::var("MVP_NODE_IMAGE_PRUNE_KEEP")
.ok() .ok()
.and_then(|value| value.trim().parse::<usize>().ok()) .and_then(|value| value.trim().parse::<usize>().ok())
.unwrap_or(3); .unwrap_or(3);
@ -554,7 +554,7 @@ fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
} }
if docker_image_has_container(root, &image_ref) { if docker_image_has_container(root, &image_ref) {
eprintln!( eprintln!(
"myelin-node-image: prune old dirty image {image_ref} skipped: container exists" "mvp-node-image: prune old dirty image {image_ref} skipped: container exists"
); );
continue; continue;
} }
@ -563,9 +563,9 @@ fn prune_old_dirty_images(root: &Path, image: &ImageName, keep_tag: &str) {
continue; continue;
} }
eprintln!("myelin-node-image: prune old dirty image {image_ref}"); eprintln!("mvp-node-image: prune old dirty image {image_ref}");
if let Err(error) = docker_image_remove(root, &image_ref) { if let Err(error) = docker_image_remove(root, &image_ref) {
eprintln!("myelin-node-image: prune old dirty image {image_ref} skipped: {error}"); eprintln!("mvp-node-image: prune old dirty image {image_ref} skipped: {error}");
} }
} }
} }
@ -669,7 +669,7 @@ fn run_status_command(
progress: &mut Option<&mut dyn NodeImageProgressSink>, progress: &mut Option<&mut dyn NodeImageProgressSink>,
) -> Result<(), String> { ) -> Result<(), String> {
let args: Vec<String> = args.iter().map(|arg| (*arg).to_owned()).collect(); let args: Vec<String> = args.iter().map(|arg| (*arg).to_owned()).collect();
eprintln!("myelin-node-image: {label}"); eprintln!("mvp-node-image: {label}");
if progress.is_none() { if progress.is_none() {
let status = Command::new(program) let status = Command::new(program)
.current_dir(root) .current_dir(root)

View file

@ -38,14 +38,14 @@ use iroh_driver::EndpointAddrMask;
const DEFAULT_CONFIG_PATH: &str = ".config/config.toml"; const DEFAULT_CONFIG_PATH: &str = ".config/config.toml";
const DEFAULT_RPC_ADDR: &str = "127.0.0.1:19777"; const DEFAULT_RPC_ADDR: &str = "127.0.0.1:19777";
const BASE_NODE_IMAGE: &str = "myelin-node-base:cuda12.6"; const BASE_NODE_IMAGE: &str = "swactor-mvp-node-base:cuda12.6";
const REPO_MODEL_CACHE_DIR: &str = ".model-cache"; const REPO_MODEL_CACHE_DIR: &str = ".model-cache";
const DEFAULT_MAX_TOKENS: u32 = 64; const DEFAULT_MAX_TOKENS: u32 = 64;
const ORCH_SHUTDOWN_GRACE_MS: u64 = 5_000; const ORCH_SHUTDOWN_GRACE_MS: u64 = 5_000;
const VASTAI_ORCH_SHUTDOWN_GRACE_MS: u64 = 180_000; const VASTAI_ORCH_SHUTDOWN_GRACE_MS: u64 = 180_000;
const MYELIN_CHAT_GPU_RUN_ENV: &str = "MYELIN_CHAT_GPU_RUN"; const MVP_CHAT_GPU_RUN_ENV: &str = "MVP_CHAT_GPU_RUN";
const MYELIN_CHAT_USAGE: &str = "\ const MVP_CHAT_USAGE: &str = "\
USAGE: cargo myelin-chat [OPTIONS] USAGE: cargo mvp-chat [OPTIONS]
OPTIONS: OPTIONS:
--gpu Run the local GPU path: in-process orchestrator plus DEV=CUDA worker selection --gpu Run the local GPU path: in-process orchestrator plus DEV=CUDA worker selection
@ -54,20 +54,20 @@ OPTIONS:
--config <path> Load config overlay --config <path> Load config overlay
--pipeline-stages|--pipeline-parallel <count> --pipeline-stages|--pipeline-parallel <count>
--relay-mode <mode> Relay mode: default or disabled --relay-mode <mode> Relay mode: default or disabled
--relay-url <url> Custom relay URL passed to myelin-orchestrator --relay-url <url> Custom relay URL passed to mvp-orchestrator
--endpoint-addr-mask <mask> Endpoint address mask: full or relay-only --endpoint-addr-mask <mask> Endpoint address mask: full or relay-only
--cached-model[=<path>] Use discovered or explicit cached GGUF model (default for --process) --cached-model[=<path>] Use discovered or explicit cached GGUF model
--dump-logs[=<path>] Write datastream frame log --dump-logs[=<path>] Write datastream frame log
--run-id <id> Override run id --run-id <id> Override run id
--skip-rebuild Reuse existing Cargo artifacts --skip-rebuild Reuse existing Cargo artifacts
--yes, -y Approve Vast.ai lease prompts --yes, -y Approve Vast.ai lease prompts
--help, -h Print this help"; --help, -h Print this help";
const ORCH_SHUTDOWN_POLL_MS: u64 = 50; const ORCH_SHUTDOWN_POLL_MS: u64 = 50;
const CHAT_LIFECYCLE_CHANNEL: &str = "myelin.chat.lifecycle"; const CHAT_LIFECYCLE_CHANNEL: &str = "mvp.chat.lifecycle";
const CHAT_RUNTIME_CHANNEL: &str = "myelin.chat.runtime"; const CHAT_RUNTIME_CHANNEL: &str = "mvp.chat.runtime";
const CHAT_PROMPT_CHANNEL: &str = "myelin.chat.prompt"; const CHAT_PROMPT_CHANNEL: &str = "mvp.chat.prompt";
const CHAT_COMPONENT_CHANNEL: &str = "myelin.chat.component"; const CHAT_COMPONENT_CHANNEL: &str = "mvp.chat.component";
const CHAT_BENCHMARK_CHANNEL: &str = "myelin.chat.benchmark"; const CHAT_BENCHMARK_CHANNEL: &str = "mvp.chat.benchmark";
#[derive(Debug)] #[derive(Debug)]
enum PromptInput { enum PromptInput {
@ -86,7 +86,7 @@ where
match install_signal_handlers().and_then(|()| run(args)) { match install_signal_handlers().and_then(|()| run(args)) {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(error) => { Err(error) => {
eprintln!("myelin-chat: {error}"); eprintln!("mvp-chat: {error}");
ExitCode::from(1) ExitCode::from(1)
} }
} }
@ -129,7 +129,7 @@ where
.iter() .iter()
.any(|arg| matches!(arg.as_str(), "--help" | "-h" | "help")) .any(|arg| matches!(arg.as_str(), "--help" | "-h" | "help"))
{ {
println!("{MYELIN_CHAT_USAGE}"); println!("{MVP_CHAT_USAGE}");
return Ok(()); return Ok(());
} }
let config = Config::from_args(provided_args)?; let config = Config::from_args(provided_args)?;
@ -386,11 +386,11 @@ struct ChatDatastream {
impl ChatDatastream { impl ChatDatastream {
fn new(run_id: u64, archive_path: Option<PathBuf>) -> Result<Self, String> { fn new(run_id: u64, archive_path: Option<PathBuf>) -> Result<Self, String> {
let stream = StreamId::new(NodeId::new("myelin-chat"), Lifetime(run_id)); let stream = StreamId::new(NodeId::new("mvp-chat"), Lifetime(run_id));
let endpoint = DatastreamEndpoint::with_descriptor( let endpoint = DatastreamEndpoint::with_descriptor(
StreamDescriptor { StreamDescriptor {
stream: stream.clone(), stream: stream.clone(),
label: Some("myelin chat".to_owned()), label: Some("mvp chat".to_owned()),
origin: StreamOrigin::Orchestrator, origin: StreamOrigin::Orchestrator,
}, },
1024, 1024,
@ -436,7 +436,7 @@ impl ChatDatastream {
fn emit(&mut self, channel: &str, phase: &str, status: &str, detail: Value) { fn emit(&mut self, channel: &str, phase: &str, status: &str, detail: Value) {
let id = self.channel_by_name(channel); let id = self.channel_by_name(channel);
let benchmark = benchmark::stamp("myelin-chat"); let benchmark = benchmark::stamp("mvp-chat");
let payload = serde_json::to_vec(&json!({ let payload = serde_json::to_vec(&json!({
"schema_version": benchmark["schema_version"].clone(), "schema_version": benchmark["schema_version"].clone(),
"type": "ChatProgress", "type": "ChatProgress",
@ -452,19 +452,19 @@ impl ChatDatastream {
"wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(), "wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(),
"monotonic_ms": benchmark["monotonic_ms"].clone(), "monotonic_ms": benchmark["monotonic_ms"].clone(),
"clock_source": benchmark["clock_source"].clone(), "clock_source": benchmark["clock_source"].clone(),
"span_id": format!("myelin-chat:{}:{}:{phase}", self.run_id, benchmark["producer_sequence"]), "span_id": format!("mvp-chat:{}:{}:{phase}", self.run_id, benchmark["producer_sequence"]),
"parent_span_id": Value::Null, "parent_span_id": Value::Null,
"benchmark": benchmark, "benchmark": benchmark,
"detail": detail, "detail": detail,
})) }))
.expect("serialize myelin-chat progress event"); .expect("serialize mvp-chat progress event");
self.producer.submit_bytes(id, payload); self.producer.submit_bytes(id, payload);
self.flush(); self.flush();
} }
fn emit_benchmark_envelope(&mut self, config: &Config) { fn emit_benchmark_envelope(&mut self, config: &Config) {
let id = self.channel_by_name(CHAT_BENCHMARK_CHANNEL); let id = self.channel_by_name(CHAT_BENCHMARK_CHANNEL);
let benchmark = benchmark::stamp("myelin-chat"); let benchmark = benchmark::stamp("mvp-chat");
let payload = serde_json::to_vec(&json!({ let payload = serde_json::to_vec(&json!({
"schema_version": benchmark["schema_version"].clone(), "schema_version": benchmark["schema_version"].clone(),
"type": "BenchmarkRunEnvelope", "type": "BenchmarkRunEnvelope",
@ -480,11 +480,11 @@ impl ChatDatastream {
"wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(), "wall_clock_unix_ms": benchmark["wall_clock_unix_ms"].clone(),
"monotonic_ms": benchmark["monotonic_ms"].clone(), "monotonic_ms": benchmark["monotonic_ms"].clone(),
"clock_source": benchmark["clock_source"].clone(), "clock_source": benchmark["clock_source"].clone(),
"span_id": format!("myelin-chat:{}:{}:run_envelope", self.run_id, benchmark["producer_sequence"]), "span_id": format!("mvp-chat:{}:{}:run_envelope", self.run_id, benchmark["producer_sequence"]),
"parent_span_id": Value::Null, "parent_span_id": Value::Null,
"benchmark": benchmark, "benchmark": benchmark,
"detail": { "detail": {
"scenario": "myelin-chat", "scenario": "mvp-chat",
"detail_level": "benchmark_observability_v1", "detail_level": "benchmark_observability_v1",
"workload": { "workload": {
"mode": "stdin_prompt_corpus", "mode": "stdin_prompt_corpus",
@ -533,7 +533,7 @@ impl ChatDatastream {
}, },
}, },
})) }))
.expect("serialize myelin-chat benchmark envelope"); .expect("serialize mvp-chat benchmark envelope");
self.producer.submit_bytes(id, payload); self.producer.submit_bytes(id, payload);
self.flush(); self.flush();
} }
@ -551,7 +551,7 @@ impl ChatDatastream {
"relay_configured": config.relay_url.is_some(), "relay_configured": config.relay_url.is_some(),
"endpoint_addr_mask": config.endpoint_addr_mask.as_str(), "endpoint_addr_mask": config.endpoint_addr_mask.as_str(),
}); });
let synthetic_id = format!("myelin-chat-{}-datastream-preflight", self.run_id); let synthetic_id = format!("mvp-chat-{}-datastream-preflight", self.run_id);
for (phase, status) in [ for (phase, status) in [
("DatastreamProducerConfigured", "configured"), ("DatastreamProducerConfigured", "configured"),
("DatastreamProducerConnected", "ready"), ("DatastreamProducerConnected", "ready"),
@ -563,7 +563,7 @@ impl ChatDatastream {
phase, phase,
status, status,
json!({ json!({
"producer": "myelin-chat", "producer": "mvp-chat",
"producer_class": "rust-chat", "producer_class": "rust-chat",
"synthetic_id": synthetic_id, "synthetic_id": synthetic_id,
"datastream_endpoint": endpoint, "datastream_endpoint": endpoint,
@ -576,8 +576,8 @@ impl ChatDatastream {
"endpoint_config_snapshot", "endpoint_config_snapshot",
"ready", "ready",
json!({ json!({
"producer": "myelin-chat", "producer": "mvp-chat",
"expected_producers": ["myelin-chat", "myelin-orchestrator", "myelin-worker", "tinygrad-worker"], "expected_producers": ["mvp-chat", "mvp-orchestrator", "mvp-worker-node", "tinygrad-worker"],
"datastream_endpoint": endpoint, "datastream_endpoint": endpoint,
"runtime_endpoint": runtime_endpoint, "runtime_endpoint": runtime_endpoint,
"connectivity_preflight": { "connectivity_preflight": {
@ -597,7 +597,7 @@ impl ChatDatastream {
.cloned() .cloned()
.unwrap_or_else(|| format!("channel#{}", frame.channel.0)); .unwrap_or_else(|| format!("channel#{}", frame.channel.0));
self.pending self.pending
.push(("myelin-chat".to_owned(), stream.clone(), channel, frame)); .push(("mvp-chat".to_owned(), stream.clone(), channel, frame));
} }
} }
@ -609,7 +609,7 @@ impl ChatDatastream {
if self.pending.is_empty() { if self.pending.is_empty() {
return Ok(()); return Ok(());
} }
let mut archive = FrameArchive::open_with_label(path, "myelin-chat datastream frame log")?; let mut archive = FrameArchive::open_with_label(path, "mvp-chat datastream frame log")?;
for (source, stream, channel, frame) in self.pending.drain(..) { for (source, stream, channel, frame) in self.pending.drain(..) {
archive.record(&source, &stream, &channel, &frame)?; archive.record(&source, &stream, &channel, &frame)?;
} }
@ -790,18 +790,12 @@ impl Config {
} }
let pipeline_stages = Self::pipeline_stages(&args, &toml)?; let pipeline_stages = Self::pipeline_stages(&args, &toml)?;
let max_tokens = Self::max_tokens(&toml)?; let max_tokens = Self::max_tokens(&toml)?;
let gpu_run = args.gpu || env_flag(MYELIN_CHAT_GPU_RUN_ENV, false); let gpu_run = args.gpu || env_flag(MVP_CHAT_GPU_RUN_ENV, false);
let endpoint_addr_mask = Self::endpoint_addr_mask(&args, &toml)?; let endpoint_addr_mask = Self::endpoint_addr_mask(&args, &toml)?;
let (relay_mode, relay_url) = Self::relay_settings(&args, &toml, endpoint_addr_mask)?; let (relay_mode, relay_url) = Self::relay_settings(&args, &toml, endpoint_addr_mask)?;
let cached_model = match Self::cached_model_source(&args, &provider) { let cached_model = Self::cached_model_source(&args, gpu_run, &provider)
None => None, .map(CachedModelConfig::from_source)
Some(CachedModelSource::Path(path)) => Some(CachedModelConfig::from_path(path)?), .transpose()?;
Some(CachedModelSource::Discover) => match CachedModelConfig::discover() {
Ok(config) => Some(config),
Err(error) if args.cached_model.is_some() => return Err(error),
Err(_) => None,
},
};
let model = Self::model_config(&provider, &toml, cached_model.as_ref())?; let model = Self::model_config(&provider, &toml, cached_model.as_ref())?;
let datastream_frame_log = Self::datastream_frame_log(&args, &toml); let datastream_frame_log = Self::datastream_frame_log(&args, &toml);
let vastai = if provider == provider_kind::vastai() { let vastai = if provider == provider_kind::vastai() {
@ -811,7 +805,7 @@ impl Config {
}; };
Ok(Self { Ok(Self {
orch_bin: artifact_root().join("target/debug/myelin-orchestrator"), orch_bin: artifact_root().join("target/debug/mvp-orchestrator"),
worker_bin: default_worker_bin(), worker_bin: default_worker_bin(),
rpc_addr: DEFAULT_RPC_ADDR.to_owned(), rpc_addr: DEFAULT_RPC_ADDR.to_owned(),
node_image, node_image,
@ -882,15 +876,16 @@ impl Config {
Ok((relay_mode, relay_url)) Ok((relay_mode, relay_url))
} }
/// Resolve the cached-model source. The process provider defaults to fn cached_model_source(
/// best-effort discovery of `.model-cache/` so `cargo myelin-chat` runs a args: &ParsedArgs,
/// cached GGUF model without explicit flags. Explicit `--cached-model` is gpu_run: bool,
/// always honored (and stays strict); the default degrades gracefully to provider: &ProviderKind,
/// the normal download path when no cached model is present. ) -> Option<CachedModelSource> {
fn cached_model_source(args: &ParsedArgs, provider: &ProviderKind) -> Option<CachedModelSource> {
match &args.cached_model { match &args.cached_model {
Some(source) => Some(source.clone()), Some(source) => Some(source.clone()),
None if provider == &provider_kind::process() => Some(CachedModelSource::Discover), None if gpu_run && provider == &provider_kind::process() => {
Some(CachedModelSource::Discover)
}
None => None, None => None,
} }
} }
@ -916,7 +911,7 @@ impl Config {
return Some( return Some(
args.dump_log_path args.dump_log_path
.clone() .clone()
.unwrap_or_else(|| PathBuf::from("myelin-chat.log")), .unwrap_or_else(|| PathBuf::from("mvp-chat.log")),
); );
} }
if !toml.observability.dump_logs.unwrap_or(false) { if !toml.observability.dump_logs.unwrap_or(false) {
@ -925,7 +920,7 @@ impl Config {
Some( Some(
first_non_empty([toml.observability.dump_log_path.clone()]) first_non_empty([toml.observability.dump_log_path.clone()])
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("myelin-chat.log")), .unwrap_or_else(|| PathBuf::from("mvp-chat.log")),
) )
} }
@ -1196,7 +1191,7 @@ impl ParsedArgs {
{ {
continue; continue;
} }
return Err(format!("unsupported myelin-chat argument {arg:?}")); return Err(format!("unsupported mvp-chat argument {arg:?}"));
} }
Ok(parsed) Ok(parsed)
} }
@ -1655,16 +1650,16 @@ where
true, true,
config.skip_rebuild, config.skip_rebuild,
&config.orch_bin, &config.orch_bin,
"myelin-orchestrator", "mvp-orchestrator",
&[ &[
"build", "build",
"--quiet", "--quiet",
"-p", "-p",
"myelin", "mvp-system",
"--features", "--features",
"dashboard", "dashboard",
"--bin", "--bin",
"myelin-orchestrator", "mvp-orchestrator",
], ],
)?; )?;
} }
@ -1677,14 +1672,14 @@ where
false, false,
config.skip_rebuild, config.skip_rebuild,
&config.worker_bin, &config.worker_bin,
"myelin-worker", "mvp-worker-node",
&[ &[
"build", "build",
"--quiet", "--quiet",
"-p", "-p",
"myelin", "mvp-system",
"--bin", "--bin",
"myelin-worker", "mvp-worker-node",
], ],
)?; )?;
emit_chat_progress( emit_chat_progress(
@ -1722,14 +1717,14 @@ where
true, true,
config.skip_rebuild, config.skip_rebuild,
&config.worker_bin, &config.worker_bin,
"myelin-worker", "mvp-worker-node",
&[ &[
"build", "build",
"--quiet", "--quiet",
"-p", "-p",
"myelin", "mvp-system",
"--bin", "--bin",
"myelin-worker", "mvp-worker-node",
], ],
)?; )?;
emit_chat_progress( emit_chat_progress(
@ -2067,7 +2062,7 @@ fn run_chat_session_with_output_and_progress(
} }
fn default_worker_bin() -> PathBuf { fn default_worker_bin() -> PathBuf {
artifact_root().join("target/debug/myelin-worker") artifact_root().join("target/debug/mvp-worker-node")
} }
fn ensure_runtime_binary( fn ensure_runtime_binary(
@ -2127,6 +2122,13 @@ struct CachedModelConfig {
} }
impl CachedModelConfig { impl CachedModelConfig {
fn from_source(source: CachedModelSource) -> Result<Self, String> {
match source {
CachedModelSource::Discover => Self::discover(),
CachedModelSource::Path(path) => Self::from_path(path),
}
}
fn from_path(path: PathBuf) -> Result<Self, String> { fn from_path(path: PathBuf) -> Result<Self, String> {
let metadata = fs::metadata(&path) let metadata = fs::metadata(&path)
.map_err(|e| format!("stat cached model {}: {e}", path.display()))?; .map_err(|e| format!("stat cached model {}: {e}", path.display()))?;
@ -2263,9 +2265,9 @@ mod tests {
const PROCESS_ENV_KEYS: &[&str] = &[ const PROCESS_ENV_KEYS: &[&str] = &[
"VAST_API_KEY", "VAST_API_KEY",
"MYELIN_PIPELINE_STAGES", "MVP_PIPELINE_STAGES",
"MYELIN_RUNTIME_CONFIG", "MVP_RUNTIME_CONFIG",
"MYELIN_CHAT_GPU_RUN", "MVP_CHAT_GPU_RUN",
"DEV", "DEV",
]; ];
@ -2277,7 +2279,7 @@ mod tests {
fn new(label: &str) -> Self { fn new(label: &str) -> Self {
let id = NEXT_TEMP_ID.fetch_add(1, AtomicOrdering::SeqCst); let id = NEXT_TEMP_ID.fetch_add(1, AtomicOrdering::SeqCst);
let path = std::env::temp_dir().join(format!( let path = std::env::temp_dir().join(format!(
"myelin-chat-test-{}-{}-{}", "mvp-chat-test-{}-{}-{}",
std::process::id(), std::process::id(),
id, id,
label label
@ -2462,8 +2464,8 @@ mod tests {
with_process_state( with_process_state(
&[ &[
("MYELIN_PIPELINE_STAGES", Some("9")), ("MVP_PIPELINE_STAGES", Some("9")),
("MYELIN_RUNTIME_CONFIG", Some("local")), ("MVP_RUNTIME_CONFIG", Some("local")),
], ],
Some(temp.path()), Some(temp.path()),
|| { || {
@ -2515,34 +2517,6 @@ tag = " alias "
); );
} }
#[test]
fn config_defaults_use_cached_model_for_process_when_present() {
let temp = TempDir::new("cached-model-default");
with_process_state(&[], Some(temp.path()), || {
let cache_dir = temp.path().join(REPO_MODEL_CACHE_DIR);
fs::create_dir_all(&cache_dir).expect("create model cache dir");
fs::write(
cache_dir.join(DEFAULT_PIPELINE_CACHED_MODEL_FILE),
Vec::<u8>::new(),
)
.expect("seed cached model file");
let defaults = Config::from_args(Vec::<String>::new()).expect("defaults resolve");
assert_eq!(defaults.provider, provider_kind::process());
let cached_model = defaults
.cached_model
.expect("process provider discovers a cached model by default");
assert!(
cached_model
.host_path
.ends_with(DEFAULT_PIPELINE_CACHED_MODEL_FILE),
"discovered the seeded cached model"
);
});
}
#[test] #[test]
fn config_max_tokens_drives_orchestrator_args_and_submit_prompt() { fn config_max_tokens_drives_orchestrator_args_and_submit_prompt() {
let temp = TempDir::new("config-max-tokens"); let temp = TempDir::new("config-max-tokens");

View file

@ -1,11 +1,11 @@
//! Registers wire codecs for every Myelin actor message type. //! Registers wire codecs for every MVP actor message type.
//! //!
//! The runtime's [`CodecRegistry`](swactor_transport::CodecRegistry) needs an //! The runtime's [`CodecRegistry`](swactor_transport::CodecRegistry) needs an
//! encoder/decoder entry for each inter-node message type. This is the single //! encoder/decoder entry for each inter-node message type. This is the single
//! aggregator that wires up the node, orchestrator, prompt, and datastream //! aggregator that wires up the node, orchestrator, prompt, and datastream
//! publisher codecs. //! publisher codecs.
pub(crate) fn register_myelin_actor_codecs(registry: &mut swactor_transport::CodecRegistry) { pub(crate) fn register_mvp_actor_codecs(registry: &mut swactor_transport::CodecRegistry) {
crate::node_actor::register_codecs(registry); crate::node_actor::register_codecs(registry);
crate::orchestration::actor::register_codecs(registry); crate::orchestration::actor::register_codecs(registry);
datastream::register_datastream_publisher_codec(registry); datastream::register_datastream_publisher_codec(registry);

View file

@ -1,7 +1,7 @@
#![recursion_limit = "256"] #![recursion_limit = "256"]
#[cfg(test)] #[cfg(test)]
extern crate self as myelin; extern crate self as mvp_system;
const DEFAULT_PIPELINE_CACHED_MODEL_FILE: &str = "SmolLM2-135M-Instruct.Q4_0.gguf"; const DEFAULT_PIPELINE_CACHED_MODEL_FILE: &str = "SmolLM2-135M-Instruct.Q4_0.gguf";
const DEFAULT_PIPELINE_CACHED_MODEL_REPO: &str = "QuantFactory/SmolLM2-135M-Instruct-GGUF"; const DEFAULT_PIPELINE_CACHED_MODEL_REPO: &str = "QuantFactory/SmolLM2-135M-Instruct-GGUF";

View file

@ -170,19 +170,6 @@ pub(crate) enum NodeAgentMsg {
WorkerCrashed { WorkerCrashed {
reason: Option<String>, reason: Option<String>,
}, },
StepFailed {
step_id: u64,
},
ObjectFailed {
edge_id: u64,
object_id: Option<u64>,
},
OutputFault {
edge_id: u64,
},
EdgeFault {
edge_id: u64,
},
StopRun { StopRun {
run_id: u64, run_id: u64,
}, },
@ -221,7 +208,7 @@ pub(crate) enum NodeAgentMsg {
impl NetworkMessage for NodeAgentMsg { impl NetworkMessage for NodeAgentMsg {
fn type_tag() -> &'static str { fn type_tag() -> &'static str {
"myelin::NodeAgentMsg" "mvp_system::NodeAgentMsg"
} }
} }
@ -249,6 +236,9 @@ pub(crate) enum StageCommandWire {
layer_end_exclusive: u32, layer_end_exclusive: u32,
stage_shard_plan: Option<StageShardPlan>, stage_shard_plan: Option<StageShardPlan>,
}, },
RewireEdge {
edge_id: u64,
},
ExecuteStep { ExecuteStep {
step_id: u64, step_id: u64,
input_edge_id: u64, input_edge_id: u64,
@ -324,7 +314,7 @@ pub(crate) enum NodeAgentReport {
impl NetworkMessage for NodeAgentReport { impl NetworkMessage for NodeAgentReport {
fn type_tag() -> &'static str { fn type_tag() -> &'static str {
"myelin::NodeAgentReport" "mvp_system::NodeAgentReport"
} }
} }
@ -541,27 +531,6 @@ impl NodeAgentActor {
self.last_worker_crash = reason; self.last_worker_crash = reason;
self.core.observe(stage::StageEvent::WorkerCrashed) self.core.observe(stage::StageEvent::WorkerCrashed)
} }
NodeAgentMsg::StepFailed { step_id } => {
self.core.observe(stage::StageEvent::StepFailed {
step_id: stage::StepId(step_id),
})
}
NodeAgentMsg::ObjectFailed { edge_id, object_id } => {
self.core.observe(stage::StageEvent::ObjectFailed {
edge_id: stage::EdgeId(edge_id),
object_id: object_id.map(stage::ObjectId),
})
}
NodeAgentMsg::OutputFault { edge_id } => {
self.core.observe(stage::StageEvent::OutputFault {
edge_id: stage::EdgeId(edge_id),
})
}
NodeAgentMsg::EdgeFault { edge_id } => {
self.core.observe(stage::StageEvent::EdgeFault {
edge_id: stage::EdgeId(edge_id),
})
}
NodeAgentMsg::StopRun { run_id } => self.core.observe(stage::StageEvent::StopRun { NodeAgentMsg::StopRun { run_id } => self.core.observe(stage::StageEvent::StopRun {
run_id: stage::RunId(run_id), run_id: stage::RunId(run_id),
}), }),
@ -723,6 +692,7 @@ impl From<&stage::StageCommand> for StageCommandWire {
layer_end_exclusive: range.end_exclusive, layer_end_exclusive: range.end_exclusive,
stage_shard_plan: shard_plan.clone(), stage_shard_plan: shard_plan.clone(),
}, },
stage::StageCommand::RewireEdge { edge_id } => Self::RewireEdge { edge_id: edge_id.0 },
stage::StageCommand::ExecuteStep(step) => Self::ExecuteStep { stage::StageCommand::ExecuteStep(step) => Self::ExecuteStep {
step_id: step.step_id.0, step_id: step.step_id.0,
input_edge_id: step.input.edge_id.0, input_edge_id: step.input.edge_id.0,

View file

@ -1,4 +1,4 @@
//! Myelin node-local runtime public surface. //! MVP node-local runtime public surface.
//! //!
//! Worker-node runtime behavior lives behind this module boundary; binaries //! Worker-node runtime behavior lives behind this module boundary; binaries
//! only wire entrypoints into it. //! only wire entrypoints into it.

View file

@ -1,6 +1,6 @@
//! Worker-node runtime behavior behind the `myelin::node` boundary. //! Worker-node runtime behavior behind the `mvp_system::node` boundary.
//! //!
//! The `myelin-worker` binary remains a thin entrypoint wrapper; this module //! The `mvp-worker-node` binary remains a thin entrypoint wrapper; this module
//! owns the reusable node-local runtime, helper-command, datastream archive, //! owns the reusable node-local runtime, helper-command, datastream archive,
//! debug-join, staging, transport, and worker coordination behavior. //! debug-join, staging, transport, and worker coordination behavior.
@ -22,7 +22,7 @@ use datastream::{
Lifetime, NodeId, Record, StreamDescriptor, StreamId, StreamOrigin, Lifetime, NodeId, Record, StreamDescriptor, StreamId, StreamOrigin,
}; };
use crate::codecs::register_myelin_actor_codecs; use crate::codecs::register_mvp_actor_codecs;
use crate::gguf_shard::{StageShardPlan, materialize_stage_shard_http, validate_stage_shard_cache}; use crate::gguf_shard::{StageShardPlan, materialize_stage_shard_http, validate_stage_shard_cache};
use crate::node_actor::{ use crate::node_actor::{
NodeAgentActor, NodeAgentMsg, NodeAgentReport, StageCommandWire, StageInboundEdgeWire, NodeAgentActor, NodeAgentMsg, NodeAgentReport, StageCommandWire, StageInboundEdgeWire,
@ -53,7 +53,7 @@ use swactor::actor::{ActorAddress, ActorInterface};
use swactor::runtime::{Ctx, ExternalSender}; use swactor::runtime::{Ctx, ExternalSender};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_WORKER_SCRIPT: &str = "/usr/local/share/myelin/tinygrad_worker.py"; const DEFAULT_WORKER_SCRIPT: &str = "/usr/local/share/mvp/tinygrad_worker.py";
const DEFAULT_DEVICE: &str = "CUDA"; const DEFAULT_DEVICE: &str = "CUDA";
const DEFAULT_HF_REPO: &str = "bartowski/Llama-3.2-1B-Instruct-GGUF"; const DEFAULT_HF_REPO: &str = "bartowski/Llama-3.2-1B-Instruct-GGUF";
const DEFAULT_HF_FILE: &str = "Llama-3.2-1B-Instruct-Q4_K_M.gguf"; const DEFAULT_HF_FILE: &str = "Llama-3.2-1B-Instruct-Q4_K_M.gguf";
@ -63,22 +63,22 @@ const DEFAULT_ARENA_ALIGNMENT: u64 = 64;
const PUMP_INTERVAL: Duration = Duration::from_millis(10); const PUMP_INTERVAL: Duration = Duration::from_millis(10);
const RUNTIME_READY_RETRY_INITIAL: Duration = Duration::from_millis(100); const RUNTIME_READY_RETRY_INITIAL: Duration = Duration::from_millis(100);
const RUNTIME_READY_RETRY_MAX: Duration = Duration::from_secs(2); const RUNTIME_READY_RETRY_MAX: Duration = Duration::from_secs(2);
const NODE_BOOTSTRAP_CHANNEL: &str = "myelin.node.bootstrap"; const NODE_BOOTSTRAP_CHANNEL: &str = "mvp.node.bootstrap";
const NODE_RUNTIME_CHANNEL: &str = "myelin.node.runtime"; const NODE_RUNTIME_CHANNEL: &str = "mvp.node.runtime";
const NODE_STAGE_CHANNEL: &str = "myelin.node.stage"; const NODE_STAGE_CHANNEL: &str = "mvp.node.stage";
const NODE_WORKER_CHANNEL: &str = "myelin.node.worker"; const NODE_WORKER_CHANNEL: &str = "mvp.node.worker";
const NODE_PROMPT_CHANNEL: &str = "myelin.node.prompt"; const NODE_PROMPT_CHANNEL: &str = "mvp.node.prompt";
const NODE_SHUTDOWN_CHANNEL: &str = "myelin.node.shutdown"; const NODE_SHUTDOWN_CHANNEL: &str = "mvp.node.shutdown";
const NODE_SAMPLER_CHANNEL: &str = "myelin.node.sampler"; const NODE_SAMPLER_CHANNEL: &str = "mvp.node.sampler";
const WORKER_COMMAND_WAIT_TELEMETRY_INTERVAL: Duration = Duration::from_secs(1); const WORKER_COMMAND_WAIT_TELEMETRY_INTERVAL: Duration = Duration::from_secs(1);
fn worker_benchmark_stamp(run_id: u64, node_id: u64) -> Value { fn worker_benchmark_stamp(run_id: u64, node_id: u64) -> Value {
let mut benchmark = benchmark::stamp("myelin-worker"); let mut benchmark = benchmark::stamp("mvp-worker-node");
let pid = benchmark let pid = benchmark
.get("producer_process_id") .get("producer_process_id")
.and_then(Value::as_u64) .and_then(Value::as_u64)
.unwrap_or_else(|| u64::from(std::process::id())); .unwrap_or_else(|| u64::from(std::process::id()));
let producer_instance_id = format!("myelin-worker:{run_id}:node-{node_id}:pid-{pid}"); let producer_instance_id = format!("mvp-worker-node:{run_id}:node-{node_id}:pid-{pid}");
if let Some(object) = benchmark.as_object_mut() { if let Some(object) = benchmark.as_object_mut() {
object.insert( object.insert(
"producer_instance_id".to_owned(), "producer_instance_id".to_owned(),
@ -112,7 +112,7 @@ fn node_event_payload(
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(), "wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
"monotonic_ms":benchmark["monotonic_ms"].clone(), "monotonic_ms":benchmark["monotonic_ms"].clone(),
"clock_source":benchmark["clock_source"].clone(), "clock_source":benchmark["clock_source"].clone(),
"span_id":format!("myelin-worker:{}:{}:{}:{phase}", config.run_id, config.logical_node_id, benchmark["producer_sequence"]), "span_id":format!("mvp-worker-node:{}:{}:{}:{phase}", config.run_id, config.logical_node_id, benchmark["producer_sequence"]),
"parent_span_id":Value::Null, "parent_span_id":Value::Null,
"benchmark":benchmark, "benchmark":benchmark,
"detail":detail, "detail":detail,
@ -123,7 +123,7 @@ fn emit_stdio_datastream_frame(channel: &str, payload: &Value) -> Result<(), Str
println!( println!(
"{}", "{}",
json!({ json!({
"myelin_stdio_event":1, "mvp_stdio_event":1,
"kind":"datastream_frame", "kind":"datastream_frame",
"channel":channel, "channel":channel,
"payload":payload, "payload":payload,
@ -200,7 +200,7 @@ fn debug_join_client_main(args: Vec<String>) -> ExitCode {
match serde_json::to_string(&response) { match serde_json::to_string(&response) {
Ok(line) => println!("{line}"), Ok(line) => println!("{line}"),
Err(error) => { Err(error) => {
eprintln!("myelin-worker debug-join: serialize response: {error}"); eprintln!("mvp-worker-node debug-join: serialize response: {error}");
return ExitCode::from(1); return ExitCode::from(1);
} }
} }
@ -211,11 +211,11 @@ fn debug_join_client_main(args: Vec<String>) -> ExitCode {
} }
} }
Err(DebugJoinClientError::Cli(error)) => { Err(DebugJoinClientError::Cli(error)) => {
eprintln!("myelin-worker debug-join: {error}"); eprintln!("mvp-worker-node debug-join: {error}");
ExitCode::from(2) ExitCode::from(2)
} }
Err(DebugJoinClientError::Runtime(error)) => { Err(DebugJoinClientError::Runtime(error)) => {
eprintln!("myelin-worker debug-join: {error}"); eprintln!("mvp-worker-node debug-join: {error}");
ExitCode::from(1) ExitCode::from(1)
} }
} }
@ -339,7 +339,7 @@ fn spawn_debug_join_listener(
}); });
} }
Err(error) => { Err(error) => {
eprintln!("myelin-worker debug join listener stopped: {error}"); eprintln!("mvp-worker-node debug join listener stopped: {error}");
break; break;
} }
} }
@ -483,7 +483,7 @@ fn sampler_health_payload(
"type":"SamplerHealth", "type":"SamplerHealth",
"event_type":"SamplerHealth", "event_type":"SamplerHealth",
"event_name":"host_sampler_health", "event_name":"host_sampler_health",
"schema":"myelin.node.sampler.health.v1", "schema":"mvp.node.sampler.health.v1",
"run_id":context.run_id, "run_id":context.run_id,
"node_id":context.node_id, "node_id":context.node_id,
"stage_index":context.stage_index, "stage_index":context.stage_index,
@ -494,7 +494,7 @@ fn sampler_health_payload(
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(), "wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
"monotonic_ms":benchmark["monotonic_ms"].clone(), "monotonic_ms":benchmark["monotonic_ms"].clone(),
"clock_source":benchmark["clock_source"].clone(), "clock_source":benchmark["clock_source"].clone(),
"span_id":format!("myelin-worker:{}:{}:{}:host_sampler_health", context.run_id, context.node_id, benchmark["producer_sequence"]), "span_id":format!("mvp-worker-node:{}:{}:{}:host_sampler_health", context.run_id, context.node_id, benchmark["producer_sequence"]),
"parent_span_id":Value::Null, "parent_span_id":Value::Null,
"phase":"host_sampler_health", "phase":"host_sampler_health",
"status":status, "status":status,
@ -1013,7 +1013,7 @@ impl WorkerEdgeRuntime {
); );
let step_started = Instant::now(); let step_started = Instant::now();
let mut pump = || pump_network(driver, stack); let mut pump = || pump_network(driver, stack);
let committed_bytes = match worker.execute_step( let committed_bytes = worker.execute_step(
u64::from(config.stage_index) + 1, u64::from(config.stage_index) + 1,
step_id, step_id,
object_id, object_id,
@ -1027,15 +1027,7 @@ impl WorkerEdgeRuntime {
config, config,
datastream, datastream,
&mut pump, &mut pump,
) { )?;
Ok(bytes) => bytes,
Err(e) => {
let _ = stack
.runtime
.send_to(node_actor, NodeAgentMsg::StepFailed { step_id });
return Err(e);
}
};
let helper_execute_ms = duration_ms_u64(step_started.elapsed()); let helper_execute_ms = duration_ms_u64(step_started.elapsed());
let egress_read_started = Instant::now(); let egress_read_started = Instant::now();
let record = { let record = {
@ -1043,16 +1035,9 @@ impl WorkerEdgeRuntime {
let lease = arena let lease = arena
.lookup_lease(arena::RingId(output_ring_id)) .lookup_lease(arena::RingId(output_ring_id))
.ok_or_else(|| format!("outbound ring {output_ring_id} lease missing"))?; .ok_or_else(|| format!("outbound ring {output_ring_id} lease missing"))?;
arena.read_arena(lease.layout.data_offset, committed_bytes) arena
}; .read_arena(lease.layout.data_offset, committed_bytes)
let record = match record { .map_err(|e| format!("read egress ring: {e}"))?
Ok(record) => record,
Err(e) => {
let _ = stack
.runtime
.send_to(node_actor, NodeAgentMsg::OutputFault { edge_id: outbound.edge_id });
return Err(format!("read egress ring: {e}"));
}
}; };
let egress_read_ms = duration_ms_u64(egress_read_started.elapsed()); let egress_read_ms = duration_ms_u64(egress_read_started.elapsed());
let record_bytes = record.len(); let record_bytes = record.len();
@ -1082,12 +1067,7 @@ impl WorkerEdgeRuntime {
.as_ref() .as_ref()
.ok_or_else(|| "outbound edge sender missing".to_owned())?; .ok_or_else(|| "outbound edge sender missing".to_owned())?;
let edge_send_started = Instant::now(); let edge_send_started = Instant::now();
if let Err(e) = sender.send(record) { sender.send(record)?;
let _ = stack
.runtime
.send_to(node_actor, NodeAgentMsg::OutputFault { edge_id: outbound.edge_id });
return Err(e);
}
let edge_send_ms = duration_ms_u64(edge_send_started.elapsed()); let edge_send_ms = duration_ms_u64(edge_send_started.elapsed());
node_stage( node_stage(
datastream, datastream,
@ -1151,17 +1131,8 @@ impl WorkerEdgeRuntime {
buffer.extend_from_slice(&bytes); buffer.extend_from_slice(&bytes);
let buffered_bytes = buffer.len(); let buffered_bytes = buffer.len();
let mut records = Vec::new(); let mut records = Vec::new();
loop { while let Some(record) = take_complete_ingress_record(buffer, inbound.object_spec)? {
match take_complete_ingress_record(buffer, inbound.object_spec) { records.push(record);
Ok(Some(record)) => records.push(record),
Ok(None) => break,
Err(e) => {
let _ = stack
.runtime
.send_to(node_actor, NodeAgentMsg::ObjectFailed { edge_id, object_id: None });
return Err(e);
}
}
} }
(records, buffered_bytes) (records, buffered_bytes)
}; };
@ -1200,22 +1171,14 @@ impl WorkerEdgeRuntime {
}), }),
); );
let object_load_started = Instant::now(); let object_load_started = Instant::now();
let loaded = match worker.ring_readable( let loaded = worker.ring_readable(
ring_id, ring_id,
edge_id, edge_id,
inbound.object_spec, inbound.object_spec,
config, config,
datastream, datastream,
&mut || {}, &mut || {},
) { )?;
Ok(loaded) => loaded,
Err(e) => {
let _ = stack
.runtime
.send_to(node_actor, NodeAgentMsg::ObjectFailed { edge_id, object_id: Some(record.object_id) });
return Err(e);
}
};
let object_load_ms = duration_ms_u64(object_load_started.elapsed()); let object_load_ms = duration_ms_u64(object_load_started.elapsed());
let key = ObjectKey { let key = ObjectKey {
edge_id, edge_id,
@ -1547,9 +1510,11 @@ impl WorkerEdgeRuntime {
.runtime .runtime
.send_to( .send_to(
node_actor, node_actor,
NodeAgentMsg::EdgeFault { edge_id: edge_id.0 }, NodeAgentMsg::WorkerCrashed {
reason: Some(format!("edge {} faulted: {reason:?}", edge_id.0)),
},
) )
.map_err(|e| format!("report edge fault: {e}"))?; .map_err(|e| format!("mark worker crashed after edge fault: {e}"))?;
return Err(format!("edge {} faulted: {reason:?}", edge_id.0)); return Err(format!("edge {} faulted: {reason:?}", edge_id.0));
} }
edge::EdgeLifecycleEvent::EdgeStopped { .. } => {} edge::EdgeLifecycleEvent::EdgeStopped { .. } => {}
@ -1619,7 +1584,7 @@ pub(crate) fn run_from_env() -> ExitCode {
_ => match run() { _ => match run() {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(error) => { Err(error) => {
eprintln!("myelin-worker: {error}"); eprintln!("mvp-worker-node: {error}");
ExitCode::from(1) ExitCode::from(1)
} }
}, },
@ -1671,7 +1636,7 @@ fn run() -> Result<(), String> {
boot( boot(
"process", "process",
"started", "started",
json!({"binary":"myelin-worker","pid":std::process::id()}), json!({"binary":"mvp-worker-node","pid":std::process::id()}),
)?; )?;
let tokio = match tokio::runtime::Runtime::new() { let tokio = match tokio::runtime::Runtime::new() {
@ -1722,20 +1687,17 @@ fn run() -> Result<(), String> {
boot( boot(
"coordinator_join", "coordinator_join",
"skipped", "skipped",
json!({"reason":"MYELIN_COORDINATOR_ENDPOINT not set","mode":"standalone"}), json!({"reason":"MVP_COORDINATOR_ENDPOINT not set","mode":"standalone"}),
)?; )?;
} }
let mut datastream = NodeDatastream::new(&config);
let worker_stats_hook = datastream.producer.stats_hook();
let stack = DistributionRuntimeStack::new_with_codecs( let stack = DistributionRuntimeStack::new_with_codecs(
driver.node_id(), driver.node_id(),
DistributedNodeConfig::default(), DistributedNodeConfig::default(),
|registry| { |registry| {
register_myelin_actor_codecs(registry); register_mvp_actor_codecs(registry);
datastream::wire::register_datastream_codec(registry); datastream::wire::register_datastream_codec(registry);
}, },
Some(worker_stats_hook),
); );
boot( boot(
"distribution_stack", "distribution_stack",
@ -1788,6 +1750,7 @@ fn run() -> Result<(), String> {
}; };
let arena_fd = arena_manager.lock().arena_fd(); let arena_fd = arena_manager.lock().arena_fd();
let mut datastream = NodeDatastream::new(&config);
let node_boot = |ds: &mut NodeDatastream, phase: &str, status: &str, detail: Value| { let node_boot = |ds: &mut NodeDatastream, phase: &str, status: &str, detail: Value| {
emit_node_event(ds, &config, NODE_BOOTSTRAP_CHANNEL, phase, status, detail) emit_node_event(ds, &config, NODE_BOOTSTRAP_CHANNEL, phase, status, detail)
}; };
@ -1843,7 +1806,7 @@ fn run() -> Result<(), String> {
json!({"actor":datastream_publisher,"name":DATASTREAM_PUBLISHER_NAME,"subscription_transport":"iroh"}), json!({"actor":datastream_publisher,"name":DATASTREAM_PUBLISHER_NAME,"subscription_transport":"iroh"}),
); );
let worker_synthetic_id = format!( let worker_synthetic_id = format!(
"myelin-worker-{}-{}-datastream-preflight", "mvp-worker-node-{}-{}-datastream-preflight",
config.logical_node_id, config.stage_index config.logical_node_id, config.stage_index
); );
for (phase, status) in [ for (phase, status) in [
@ -1857,7 +1820,7 @@ fn run() -> Result<(), String> {
phase, phase,
status, status,
json!({ json!({
"producer":"myelin-worker", "producer":"mvp-worker-node",
"producer_class":"rust-worker-node", "producer_class":"rust-worker-node",
"synthetic_id":worker_synthetic_id, "synthetic_id":worker_synthetic_id,
"datastream_endpoint":{ "datastream_endpoint":{
@ -1897,7 +1860,7 @@ fn run() -> Result<(), String> {
&mut datastream, &mut datastream,
"debug_join_socket", "debug_join_socket",
"skipped", "skipped",
json!({"reason":"MYELIN_DEBUG_JOIN_SOCKET=disabled"}), json!({"reason":"MVP_DEBUG_JOIN_SOCKET=disabled"}),
); );
None None
} }
@ -1918,7 +1881,7 @@ fn run() -> Result<(), String> {
} }
}; };
let orchestrator = config.orchestrator_actor.ok_or_else(|| { let orchestrator = config.orchestrator_actor.ok_or_else(|| {
"MYELIN_ORCHESTRATOR_ACTOR is required for runtime readiness signaling".to_owned() "MVP_ORCHESTRATOR_ACTOR is required for runtime readiness signaling".to_owned()
})?; })?;
let orchestrator_source = "env"; let orchestrator_source = "env";
boot( boot(
@ -2108,7 +2071,7 @@ fn run() -> Result<(), String> {
&mut datastream, &mut datastream,
"datastream_handoff", "datastream_handoff",
"ready", "ready",
json!({"from":"runtime_ready_ack","to":"cluster_datastream","channel":"myelin.node.ready"}), json!({"from":"runtime_ready_ack","to":"cluster_datastream","channel":"mvp.node.ready"}),
); );
} }
} }
@ -2251,7 +2214,7 @@ impl NodeDatastream {
let endpoint = Arc::new(DatastreamEndpoint::with_descriptor( let endpoint = Arc::new(DatastreamEndpoint::with_descriptor(
StreamDescriptor { StreamDescriptor {
stream: stream.clone(), stream: stream.clone(),
label: Some("myelin worker node".to_owned()), label: Some("mvp worker node".to_owned()),
origin: StreamOrigin::RemoteNode, origin: StreamOrigin::RemoteNode,
}, },
256, 256,
@ -2269,16 +2232,16 @@ impl NodeDatastream {
NODE_PROMPT_CHANNEL, NODE_PROMPT_CHANNEL,
NODE_SHUTDOWN_CHANNEL, NODE_SHUTDOWN_CHANNEL,
NODE_SAMPLER_CHANNEL, NODE_SAMPLER_CHANNEL,
"myelin.worker.initialize", "mvp.worker.initialize",
"myelin.worker.role", "mvp.worker.role",
"myelin.worker.weights", "mvp.worker.weights",
"myelin.worker.prompt", "mvp.worker.prompt",
"myelin.worker.tokenizer", "mvp.worker.tokenizer",
"myelin.worker.ring", "mvp.worker.ring",
"myelin.worker.ingress", "mvp.worker.ingress",
"myelin.worker.step", "mvp.worker.step",
"myelin.worker.device_object", "mvp.worker.device_object",
"myelin.worker.shutdown", "mvp.worker.shutdown",
] { ] {
register_json_channel(&producer, &mut by_name, &mut by_id, name); register_json_channel(&producer, &mut by_name, &mut by_id, name);
} }
@ -2288,25 +2251,25 @@ impl NodeDatastream {
&producer, &producer,
&mut by_name, &mut by_name,
&mut by_id, &mut by_id,
"myelin.node.ready", "mvp.node.ready",
), ),
node_lifecycle: register_json_channel( node_lifecycle: register_json_channel(
&producer, &producer,
&mut by_name, &mut by_name,
&mut by_id, &mut by_id,
"myelin.node.lifecycle", "mvp.node.lifecycle",
), ),
node_self_test: register_json_channel( node_self_test: register_json_channel(
&producer, &producer,
&mut by_name, &mut by_name,
&mut by_id, &mut by_id,
"myelin.node.self_test", "mvp.node.self_test",
), ),
worker_stderr: register_json_channel( worker_stderr: register_json_channel(
&producer, &producer,
&mut by_name, &mut by_name,
&mut by_id, &mut by_id,
"myelin.worker.stderr", "mvp.worker.stderr",
), ),
host_cpu: register_record_channel::<datastream::hardware::cpu::HostCpuSample>( host_cpu: register_record_channel::<datastream::hardware::cpu::HostCpuSample>(
&producer, &producer,
@ -3255,10 +3218,10 @@ fn publish_stage_shard_fetch_event(
config: &DeploymentConfig, config: &DeploymentConfig,
event: &Value, event: &Value,
) -> Result<(), String> { ) -> Result<(), String> {
let channel = datastream.channel_by_name("myelin.worker.weights"); let channel = datastream.channel_by_name("mvp.worker.weights");
let payload = node_event_payload(config, "stage_shard_fetch", "event", event.clone()); let payload = node_event_payload(config, "stage_shard_fetch", "event", event.clone());
datastream.submit_text(channel, payload.to_string()); datastream.submit_text(channel, payload.to_string());
emit_stdio_datastream_frame("myelin.worker.weights", &payload) emit_stdio_datastream_frame("mvp.worker.weights", &payload)
.map_err(|e| format!("emit stage shard fetch datastream frame: {e}"))?; .map_err(|e| format!("emit stage shard fetch datastream frame: {e}"))?;
datastream.tick(); datastream.tick();
Ok(()) Ok(())
@ -3271,11 +3234,11 @@ fn materialize_stage_shard_with_process(
driver: &mut IrohDriver, driver: &mut IrohDriver,
stack: &DistributionRuntimeStack, stack: &DistributionRuntimeStack,
) -> Result<PathBuf, String> { ) -> Result<PathBuf, String> {
let output_path = std::env::var("MYELIN_MODEL_CACHE_DIR") let output_path = std::env::var("MVP_MODEL_CACHE_DIR")
.ok() .ok()
.filter(|value| !value.trim().is_empty()) .filter(|value| !value.trim().is_empty())
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/var/cache/myelin-models")) .unwrap_or_else(|| PathBuf::from("/var/cache/mvp-models"))
.join(plan.cache_file_name()); .join(plan.cache_file_name());
if output_path.is_file() { if output_path.is_file() {
match validate_stage_shard_cache(&output_path, plan) { match validate_stage_shard_cache(&output_path, plan) {
@ -3597,6 +3560,15 @@ fn handle_stage_command(
); );
Ok(()) Ok(())
} }
StageCommandWire::RewireEdge { .. } => {
node_stage(
datastream,
"rewire_edge",
"skipped",
json!({"reason":"not implemented in mvp-worker-node image path"}),
);
Ok(())
}
StageCommandWire::ReleaseInputHandle { handle_id, .. } => { StageCommandWire::ReleaseInputHandle { handle_id, .. } => {
edge_runtime.release_input_handle(handle_id, worker, config, datastream, driver, stack) edge_runtime.release_input_handle(handle_id, worker, config, datastream, driver, stack)
} }
@ -3736,22 +3708,22 @@ impl DeploymentConfig {
} }
}; };
} }
let run_id = env_parse!("MYELIN_RUN_ID", 1)?; let run_id = env_parse!("MVP_RUN_ID", 1)?;
let logical_node_id = env_parse!("MYELIN_LOGICAL_NODE_ID", 1)?; let logical_node_id = env_parse!("MVP_LOGICAL_NODE_ID", 1)?;
let relay = relay_runtime_config_from_env(run_id)?; let relay = relay_runtime_config_from_env(run_id)?;
let debug_join_socket = match env_optional("MYELIN_DEBUG_JOIN_SOCKET").as_deref() { let debug_join_socket = match env_optional("MVP_DEBUG_JOIN_SOCKET").as_deref() {
Some("disabled") => None, Some("disabled") => None,
Some(path) => Some(path.to_owned()), Some(path) => Some(path.to_owned()),
None => Some( None => Some(
std::env::temp_dir() std::env::temp_dir()
.join(format!( .join(format!(
"myelin-node-debug-join-{run_id}-{logical_node_id}.sock" "mvp-node-debug-join-{run_id}-{logical_node_id}.sock"
)) ))
.to_string_lossy() .to_string_lossy()
.into_owned(), .into_owned(),
), ),
}; };
let provider = env_optional("MYELIN_NODE_PROVIDER").unwrap_or_else(|| "process".to_owned()); let provider = env_optional("MVP_NODE_PROVIDER").unwrap_or_else(|| "process".to_owned());
let default_device = if provider == "process" { let default_device = if provider == "process" {
"CPU" "CPU"
} else { } else {
@ -3760,20 +3732,20 @@ impl DeploymentConfig {
Ok(Self { Ok(Self {
run_id, run_id,
logical_node_id, logical_node_id,
stage_index: env_parse!("MYELIN_STAGE_INDEX", 0)?, stage_index: env_parse!("MVP_STAGE_INDEX", 0)?,
coordinator_endpoint: env_optional("MYELIN_COORDINATOR_ENDPOINT") coordinator_endpoint: env_optional("MVP_COORDINATOR_ENDPOINT")
.map(|value| { .map(|value| {
serde_json::from_str::<EndpointAddr>(&value) serde_json::from_str::<EndpointAddr>(&value)
.map_err(|e| format!("invalid MYELIN_COORDINATOR_ENDPOINT JSON: {e}")) .map_err(|e| format!("invalid MVP_COORDINATOR_ENDPOINT JSON: {e}"))
}) })
.transpose()?, .transpose()?,
orchestrator_actor: env_optional("MYELIN_ORCHESTRATOR_ACTOR") orchestrator_actor: env_optional("MVP_ORCHESTRATOR_ACTOR")
.map(|value| { .map(|value| {
serde_json::from_str::<ActorAddress>(&value) serde_json::from_str::<ActorAddress>(&value)
.map_err(|e| format!("invalid MYELIN_ORCHESTRATOR_ACTOR JSON: {e}")) .map_err(|e| format!("invalid MVP_ORCHESTRATOR_ACTOR JSON: {e}"))
}) })
.transpose()?, .transpose()?,
datastream_frame_log: env_optional("MYELIN_DATASTREAM_FRAME_LOG"), datastream_frame_log: env_optional("MVP_DATASTREAM_FRAME_LOG"),
debug_join_socket, debug_join_socket,
relay_mode: relay.mode, relay_mode: relay.mode,
endpoint_addr_mask: env_optional(MVP_IROH_ENDPOINT_ADDR_MASK_ENV) endpoint_addr_mask: env_optional(MVP_IROH_ENDPOINT_ADDR_MASK_ENV)
@ -3781,29 +3753,29 @@ impl DeploymentConfig {
.map(EndpointAddrMask::parse) .map(EndpointAddrMask::parse)
.transpose()? .transpose()?
.unwrap_or_default(), .unwrap_or_default(),
worker_script: env_optional("MYELIN_TINYGRAD_WORKER") worker_script: env_optional("MVP_TINYGRAD_WORKER")
.unwrap_or_else(|| DEFAULT_WORKER_SCRIPT.to_owned()), .unwrap_or_else(|| DEFAULT_WORKER_SCRIPT.to_owned()),
device: env_optional("DEV").unwrap_or_else(|| default_device.to_owned()), device: env_optional("DEV").unwrap_or_else(|| default_device.to_owned()),
model_id: env_optional("MYELIN_MODEL_ID").unwrap_or_else(|| DEFAULT_MODEL_ID.to_owned()), model_id: env_optional("MVP_MODEL_ID").unwrap_or_else(|| DEFAULT_MODEL_ID.to_owned()),
gguf_source: if let Some(path) = env_optional("MYELIN_GGUF_LOCAL_PATH") { gguf_source: if let Some(path) = env_optional("MVP_GGUF_LOCAL_PATH") {
GgufSource::LocalPath(path) GgufSource::LocalPath(path)
} else { } else {
GgufSource::HuggingFaceGguf { GgufSource::HuggingFaceGguf {
repo: env_optional("MYELIN_GGUF_REPO") repo: env_optional("MVP_GGUF_REPO")
.unwrap_or_else(|| DEFAULT_HF_REPO.to_owned()), .unwrap_or_else(|| DEFAULT_HF_REPO.to_owned()),
file: env_optional("MYELIN_GGUF_FILE") file: env_optional("MVP_GGUF_FILE")
.unwrap_or_else(|| DEFAULT_HF_FILE.to_owned()), .unwrap_or_else(|| DEFAULT_HF_FILE.to_owned()),
revision: env_optional("MYELIN_GGUF_REVISION"), revision: env_optional("MVP_GGUF_REVISION"),
} }
}, },
tokenizer: env_optional("MYELIN_TOKENIZER_LOCAL_PATH") tokenizer: env_optional("MVP_TOKENIZER_LOCAL_PATH")
.map(TokenizerSource::LocalPath) .map(TokenizerSource::LocalPath)
.unwrap_or(TokenizerSource::EmbeddedGguf), .unwrap_or(TokenizerSource::EmbeddedGguf),
self_test_prompt: env_optional("MYELIN_NODE_SELF_TEST_PROMPT"), self_test_prompt: env_optional("MVP_NODE_SELF_TEST_PROMPT"),
self_test_layer_end: env_parse!("MYELIN_SELF_TEST_LAYER_END", 16)?, self_test_layer_end: env_parse!("MVP_SELF_TEST_LAYER_END", 16)?,
self_test_max_tokens: env_parse!("MYELIN_SELF_TEST_MAX_TOKENS", 1)?, self_test_max_tokens: env_parse!("MVP_SELF_TEST_MAX_TOKENS", 1)?,
arena_bytes: env_parse!("MYELIN_ARENA_BYTES", DEFAULT_ARENA_BYTES)?, arena_bytes: env_parse!("MVP_ARENA_BYTES", DEFAULT_ARENA_BYTES)?,
arena_alignment: env_parse!("MYELIN_ARENA_ALIGNMENT", DEFAULT_ARENA_ALIGNMENT)?, arena_alignment: env_parse!("MVP_ARENA_ALIGNMENT", DEFAULT_ARENA_ALIGNMENT)?,
}) })
} }
} }
@ -4035,20 +4007,20 @@ impl TinygradWorker {
let mut child = Command::new("python3") let mut child = Command::new("python3")
.arg(&config.worker_script) .arg(&config.worker_script)
.env("DEV", &config.device) .env("DEV", &config.device)
.env("MYELIN_RUN_ID", config.run_id.to_string()) .env("MVP_RUN_ID", config.run_id.to_string())
.env("MYELIN_LOGICAL_NODE_ID", config.logical_node_id.to_string()) .env("MVP_LOGICAL_NODE_ID", config.logical_node_id.to_string())
.env("MYELIN_STAGE_INDEX", config.stage_index.to_string()) .env("MVP_STAGE_INDEX", config.stage_index.to_string())
.env("MYELIN_ARENA_FD", arena_fd.to_string()) .env("MVP_ARENA_FD", arena_fd.to_string())
.env("MYELIN_ARENA_BYTES", config.arena_bytes.to_string()) .env("MVP_ARENA_BYTES", config.arena_bytes.to_string())
.env( .env(
"MYELIN_DATASTREAM_ENDPOINT_ID", "MVP_DATASTREAM_ENDPOINT_ID",
format!( format!(
"worker-node-{}-stage-{}-stdio-bridge", "worker-node-{}-stage-{}-stdio-bridge",
config.logical_node_id, config.stage_index config.logical_node_id, config.stage_index
), ),
) )
.env( .env(
"MYELIN_BENCHMARK_PRODUCER_INSTANCE", "MVP_BENCHMARK_PRODUCER_INSTANCE",
format!( format!(
"tinygrad-worker:{}:{}", "tinygrad-worker:{}:{}",
config.logical_node_id, config.stage_index config.logical_node_id, config.stage_index
@ -4058,7 +4030,7 @@ impl TinygradWorker {
"MVP_IROH_ENDPOINT_ADDR_MASK", "MVP_IROH_ENDPOINT_ADDR_MASK",
config.endpoint_addr_mask.as_str(), config.endpoint_addr_mask.as_str(),
) )
.env("MYELIN_IROH_RELAY_MODE", format!("{:?}", config.relay_mode)) .env("MVP_IROH_RELAY_MODE", format!("{:?}", config.relay_mode))
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@ -4106,7 +4078,7 @@ impl TinygradWorker {
"WorkerReady", "WorkerReady",
config, config,
datastream, datastream,
"myelin.worker.initialize", "mvp.worker.initialize",
pump, pump,
) )
.map(|_| ()) .map(|_| ())
@ -4136,7 +4108,7 @@ impl TinygradWorker {
"RoleConfigured", "RoleConfigured",
config, config,
datastream, datastream,
"myelin.worker.role", "mvp.worker.role",
pump, pump,
) )
.map(|_| ()) .map(|_| ())
@ -4165,7 +4137,7 @@ impl TinygradWorker {
"WeightsLoaded", "WeightsLoaded",
config, config,
datastream, datastream,
"myelin.worker.weights", "mvp.worker.weights",
pump, pump,
) )
.map(|_| ()) .map(|_| ())
@ -4185,7 +4157,7 @@ impl TinygradWorker {
"PromptCompleted", "PromptCompleted",
config, config,
datastream, datastream,
"myelin.worker.prompt", "mvp.worker.prompt",
pump, pump,
) )
} }
@ -4203,7 +4175,7 @@ impl TinygradWorker {
"PromptEncoded", "PromptEncoded",
config, config,
datastream, datastream,
"myelin.worker.tokenizer", "mvp.worker.tokenizer",
pump, pump,
)?; )?;
result result
@ -4234,7 +4206,7 @@ impl TinygradWorker {
"TokensDecoded", "TokensDecoded",
config, config,
datastream, datastream,
"myelin.worker.tokenizer", "mvp.worker.tokenizer",
pump, pump,
)?; )?;
result result
@ -4279,7 +4251,7 @@ impl TinygradWorker {
"RingInstalled", "RingInstalled",
config, config,
datastream, datastream,
"myelin.worker.ring", "mvp.worker.ring",
pump, pump,
) )
.map(|_| ()) .map(|_| ())
@ -4297,7 +4269,7 @@ impl TinygradWorker {
"RingUninstalled", "RingUninstalled",
config, config,
datastream, datastream,
"myelin.worker.ring", "mvp.worker.ring",
pump, pump,
) )
.map(|_| ()) .map(|_| ())
@ -4325,7 +4297,7 @@ impl TinygradWorker {
"ObjectLoaded", "ObjectLoaded",
config, config,
datastream, datastream,
"myelin.worker.ingress", "mvp.worker.ingress",
pump, pump,
)?; )?;
Ok(LoadedObject { Ok(LoadedObject {
@ -4373,7 +4345,7 @@ impl TinygradWorker {
"StepExecuted", "StepExecuted",
config, config,
datastream, datastream,
"myelin.worker.step", "mvp.worker.step",
pump, pump,
)?; )?;
usize::try_from(value_u64(&event, "committed_bytes")?) usize::try_from(value_u64(&event, "committed_bytes")?)
@ -4392,7 +4364,7 @@ impl TinygradWorker {
"DeviceObjectReleased", "DeviceObjectReleased",
config, config,
datastream, datastream,
"myelin.worker.device_object", "mvp.worker.device_object",
pump, pump,
) )
.map(|_| ()) .map(|_| ())
@ -4409,7 +4381,7 @@ impl TinygradWorker {
"WorkerStopped", "WorkerStopped",
config, config,
datastream, datastream,
"myelin.worker.shutdown", "mvp.worker.shutdown",
pump, pump,
) )
.map(|_| ()) .map(|_| ())

View file

@ -1,4 +1,4 @@
//! Dashboard view over Myelin cluster provisioning and lifecycle datastream records. //! Dashboard view over MVP cluster provisioning and lifecycle datastream records.
use std::collections::{BTreeMap, VecDeque}; use std::collections::{BTreeMap, VecDeque};
@ -12,29 +12,29 @@ use std::sync::RwLock;
use crate::observability::lifecycle as obs; use crate::observability::lifecycle as obs;
use crate::observability::telemetry::{ use crate::observability::telemetry::{
MYELIN_LIFECYCLE, MYELIN_PROVISIONING_EVENTS, MYELIN_PROVISIONING_LOGS, MyelinLifecycleRecord, MVP_LIFECYCLE, MVP_PROVISIONING_EVENTS, MVP_PROVISIONING_LOGS, MvpLifecycleRecord,
MyelinProvisionEventRecord, MyelinProvisionLogRecord, MvpProvisionEventRecord, MvpProvisionLogRecord,
}; };
use crate::provisioning::{ProvisionEventKind, ProvisionLogStream}; use crate::provisioning::{ProvisionEventKind, ProvisionLogStream};
const CHANNELS: &[&str] = &[]; const CHANNELS: &[&str] = &[];
const PROVISIONING_LOG_PREFIX: &str = "myelin.provisioning.logs.node."; const PROVISIONING_LOG_PREFIX: &str = "mvp.provisioning.logs.node.";
const EVENT_LOG_CAP: usize = 256; const EVENT_LOG_CAP: usize = 256;
const LOG_TAIL_CAP: usize = 128; const LOG_TAIL_CAP: usize = 128;
#[derive(Default)] #[derive(Default)]
pub(crate) struct MyelinClusterDashboardView { pub(crate) struct MvpClusterDashboardView {
state: RwLock<MyelinClusterDashboardState>, state: RwLock<MvpClusterDashboardState>,
} }
impl MyelinClusterDashboardView { impl MvpClusterDashboardView {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Self::default() Self::default()
} }
} }
#[derive(Default, Serialize)] #[derive(Default, Serialize)]
struct MyelinClusterDashboardState { struct MvpClusterDashboardState {
events: VecDeque<DashboardEventEntry>, events: VecDeque<DashboardEventEntry>,
nodes: BTreeMap<u64, ProvisionNodeView>, nodes: BTreeMap<u64, ProvisionNodeView>,
} }
@ -58,7 +58,7 @@ struct ProvisionNodeView {
provider_tail: VecDeque<String>, provider_tail: VecDeque<String>,
} }
impl MyelinClusterDashboardState { impl MvpClusterDashboardState {
fn push_event(&mut self, entry: DashboardEventEntry) { fn push_event(&mut self, entry: DashboardEventEntry) {
if self.events.len() == EVENT_LOG_CAP { if self.events.len() == EVENT_LOG_CAP {
self.events.pop_front(); self.events.pop_front();
@ -66,7 +66,7 @@ impl MyelinClusterDashboardState {
self.events.push_back(entry); self.events.push_back(entry);
} }
fn apply_provision_event(&mut self, frame: &Frame, record: MyelinProvisionEventRecord) { fn apply_provision_event(&mut self, frame: &Frame, record: MvpProvisionEventRecord) {
let phase = provision_kind_label(record.event.kind); let phase = provision_kind_label(record.event.kind);
let node = self let node = self
.nodes .nodes
@ -78,7 +78,7 @@ impl MyelinClusterDashboardState {
node.phase = phase.to_owned(); node.phase = phase.to_owned();
node.last_message = record.event.message.clone(); node.last_message = record.event.message.clone();
self.push_event(DashboardEventEntry { self.push_event(DashboardEventEntry {
channel: MYELIN_PROVISIONING_EVENTS.to_owned(), channel: MVP_PROVISIONING_EVENTS.to_owned(),
position: frame.position.0, position: frame.position.0,
label: phase.to_owned(), label: phase.to_owned(),
run_id: Some(record.event.run_id), run_id: Some(record.event.run_id),
@ -86,7 +86,7 @@ impl MyelinClusterDashboardState {
}); });
} }
fn apply_provision_log(&mut self, record: MyelinProvisionLogRecord) { fn apply_provision_log(&mut self, record: MvpProvisionLogRecord) {
let node = self let node = self
.nodes .nodes
.entry(record.line.node_id) .entry(record.line.node_id)
@ -105,10 +105,10 @@ impl MyelinClusterDashboardState {
tail.push_back(record.line.line); tail.push_back(record.line.line);
} }
fn apply_lifecycle(&mut self, frame: &Frame, record: MyelinLifecycleRecord) { fn apply_lifecycle(&mut self, frame: &Frame, record: MvpLifecycleRecord) {
let (run_id, node_id) = lifecycle_ids(&record.event); let (run_id, node_id) = lifecycle_ids(&record.event);
self.push_event(DashboardEventEntry { self.push_event(DashboardEventEntry {
channel: MYELIN_LIFECYCLE.to_owned(), channel: MVP_LIFECYCLE.to_owned(),
position: frame.position.0, position: frame.position.0,
label: format!("{:?}", record.event.kind()), label: format!("{:?}", record.event.kind()),
run_id, run_id,
@ -117,13 +117,13 @@ impl MyelinClusterDashboardState {
} }
} }
impl DashboardView for MyelinClusterDashboardView { impl DashboardView for MvpClusterDashboardView {
fn id(&self) -> &'static str { fn id(&self) -> &'static str {
"myelin/cluster" "mvp/cluster"
} }
fn title(&self) -> &'static str { fn title(&self) -> &'static str {
"Myelin cluster" "MVP cluster"
} }
fn channels(&self) -> &'static [&'static str] { fn channels(&self) -> &'static [&'static str] {
@ -131,23 +131,23 @@ impl DashboardView for MyelinClusterDashboardView {
} }
fn ingest(&self, _stream: &StreamId, frame: &Frame, event: &FrameEvent) { fn ingest(&self, _stream: &StreamId, frame: &Frame, event: &FrameEvent) {
let mut state = self.state.write().expect("Myelin dashboard state poisoned"); let mut state = self.state.write().expect("MVP dashboard state poisoned");
match event.channel.as_str() { match event.channel.as_str() {
MYELIN_PROVISIONING_EVENTS => { MVP_PROVISIONING_EVENTS => {
if let Ok(record) = MyelinProvisionEventRecord::decode(&event.payload) { if let Ok(record) = MvpProvisionEventRecord::decode(&event.payload) {
state.apply_provision_event(frame, record); state.apply_provision_event(frame, record);
} }
} }
channel channel
if channel == MYELIN_PROVISIONING_LOGS if channel == MVP_PROVISIONING_LOGS
|| channel.starts_with(PROVISIONING_LOG_PREFIX) => || channel.starts_with(PROVISIONING_LOG_PREFIX) =>
{ {
if let Ok(record) = MyelinProvisionLogRecord::decode(&event.payload) { if let Ok(record) = MvpProvisionLogRecord::decode(&event.payload) {
state.apply_provision_log(record); state.apply_provision_log(record);
} }
} }
MYELIN_LIFECYCLE => { MVP_LIFECYCLE => {
if let Ok(record) = MyelinLifecycleRecord::decode(&event.payload) { if let Ok(record) = MvpLifecycleRecord::decode(&event.payload) {
state.apply_lifecycle(frame, record); state.apply_lifecycle(frame, record);
} }
} }
@ -156,7 +156,7 @@ impl DashboardView for MyelinClusterDashboardView {
} }
fn snapshot_json(&self) -> Value { fn snapshot_json(&self) -> Value {
let state = self.state.read().expect("Myelin dashboard state poisoned"); let state = self.state.read().expect("MVP dashboard state poisoned");
json!({ json!({
"events": state.events, "events": state.events,
"nodes": state.nodes.values().collect::<Vec<_>>(), "nodes": state.nodes.values().collect::<Vec<_>>(),
@ -164,7 +164,7 @@ impl DashboardView for MyelinClusterDashboardView {
} }
fn html(&self) -> Option<&'static str> { fn html(&self) -> Option<&'static str> {
Some(MYELIN_CLUSTER_HTML) Some(MVP_CLUSTER_HTML)
} }
} }
@ -193,11 +193,11 @@ fn lifecycle_ids(event: &obs::Event) -> (Option<u64>, Option<u64>) {
} }
} }
const MYELIN_CLUSTER_HTML: &str = r#"<!doctype html> const MVP_CLUSTER_HTML: &str = r#"<!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Myelin cluster</title> <title>MVP cluster</title>
<style> <style>
body { font: 13px system-ui, sans-serif; margin: 1rem; background: #0f1115; color: #e8e8e8; } body { font: 13px system-ui, sans-serif; margin: 1rem; background: #0f1115; color: #e8e8e8; }
table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; } table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; }
@ -211,7 +211,7 @@ const MYELIN_CLUSTER_HTML: &str = r#"<!doctype html>
</style> </style>
</head> </head>
<body> <body>
<h1>Myelin cluster</h1> <h1>MVP cluster</h1>
<p id="status" class="muted">loading…</p> <p id="status" class="muted">loading…</p>
<h2>Provisioned nodes</h2> <h2>Provisioned nodes</h2>
<table><thead><tr><th>node</th><th>phase</th><th>message</th></tr></thead><tbody id="nodes"></tbody></table> <table><thead><tr><th>node</th><th>phase</th><th>message</th></tr></thead><tbody id="nodes"></tbody></table>
@ -222,7 +222,7 @@ const MYELIN_CLUSTER_HTML: &str = r#"<!doctype html>
<script> <script>
let selected = null; let selected = null;
async function refresh() { async function refresh() {
const data = await fetch('/api/view/myelin/cluster').then(r => r.json()); const data = await fetch('/api/view/mvp/cluster').then(r => r.json());
document.getElementById('status').textContent = `${data.nodes.length} node(s), ${data.events.length} event(s)`; document.getElementById('status').textContent = `${data.nodes.length} node(s), ${data.events.length} event(s)`;
const nodes = document.getElementById('nodes'); const nodes = document.getElementById('nodes');
nodes.innerHTML = data.nodes.map(n => `<tr data-node="${n.node_id}"><td>${n.node_id}</td><td class="${n.phase === 'provision_failed' ? 'bad' : 'ok'}">${n.phase || ''}</td><td>${n.last_message || ''}</td></tr>`).join('') || '<tr><td colspan="3" class="muted">No provisioning records yet.</td></tr>'; nodes.innerHTML = data.nodes.map(n => `<tr data-node="${n.node_id}"><td>${n.node_id}</td><td class="${n.phase === 'provision_failed' ? 'bad' : 'ok'}">${n.phase || ''}</td><td>${n.last_message || ''}</td></tr>`).join('') || '<tr><td colspan="3" class="muted">No provisioning records yet.</td></tr>';

View file

@ -7,9 +7,9 @@ use serde_json::json;
use crate::observability::benchmark; use crate::observability::benchmark;
/// JSONL archive for Myelin-owned datastream frames. /// JSONL archive for MVP-owned datastream frames.
/// ///
/// The datastream crate owns frame transport; this helper owns the Myelin archive /// The datastream crate owns frame transport; this helper owns the MVP archive
/// record shape used as benchmark and contract evidence. /// record shape used as benchmark and contract evidence.
pub(crate) struct FrameArchive { pub(crate) struct FrameArchive {
file: File, file: File,

View file

@ -1,4 +1,4 @@
//! Myelin observability public surface. //! MVP observability public surface.
pub(crate) mod benchmark; pub(crate) mod benchmark;
#[cfg(feature = "dashboard")] #[cfg(feature = "dashboard")]

View file

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use crate::observability::telemetry::{ use crate::observability::telemetry::{
MYELIN_PROVISIONING_LOGS, MyelinProvisionLogRecord, myelin_provision_log_channel, MVP_PROVISIONING_LOGS, MvpProvisionLogRecord, mvp_provision_log_channel,
}; };
use crate::provisioning::{ use crate::provisioning::{
NodeProvisionSpec, PluginObservation, PluginSink, ProvisionLogLine, ProvisionLogStream, NodeProvisionSpec, PluginObservation, PluginSink, ProvisionLogLine, ProvisionLogStream,
@ -135,16 +135,16 @@ impl BootstrapDatastreamBridge {
let Some(producer) = &self.producer else { let Some(producer) = &self.producer else {
return; return;
}; };
let record = MyelinProvisionLogRecord::new(ProvisionLogLine { let record = MvpProvisionLogRecord::new(ProvisionLogLine {
run_id: self.spec.run_id, run_id: self.spec.run_id,
node_id: self.spec.node_id, node_id: self.spec.node_id,
stream, stream,
line: line.to_owned(), line: line.to_owned(),
}); });
let channel = producer.register_channel( let channel = producer.register_channel(
myelin_provision_log_channel(self.spec.node_id, stream), mvp_provision_log_channel(self.spec.node_id, stream),
ChannelContent::JsonRecord { ChannelContent::JsonRecord {
schema: Some(MYELIN_PROVISIONING_LOGS.to_owned()), schema: Some(MVP_PROVISIONING_LOGS.to_owned()),
}, },
); );
let payload = serde_json::to_vec(&record).expect("serialize bootstrap log record"); let payload = serde_json::to_vec(&record).expect("serialize bootstrap log record");
@ -154,7 +154,7 @@ impl BootstrapDatastreamBridge {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct StdioDatastreamFrame { struct StdioDatastreamFrame {
myelin_stdio_event: u32, mvp_stdio_event: u32,
kind: String, kind: String,
channel: String, channel: String,
payload: Value, payload: Value,
@ -165,7 +165,7 @@ pub(crate) fn parse_stdio_datastream_frame(
line: &str, line: &str,
) -> Option<PluginObservation> { ) -> Option<PluginObservation> {
let frame = serde_json::from_str::<StdioDatastreamFrame>(line).ok()?; let frame = serde_json::from_str::<StdioDatastreamFrame>(line).ok()?;
if frame.myelin_stdio_event != 1 || frame.kind != "datastream_frame" { if frame.mvp_stdio_event != 1 || frame.kind != "datastream_frame" {
return None; return None;
} }
Some(PluginObservation::DatastreamFrame { Some(PluginObservation::DatastreamFrame {

View file

@ -1,5 +1,5 @@
//! Myelin-system-owned datastream channel records. //! MVP-system-owned datastream channel records.
use datastream::Record; use datastream::Record;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -7,61 +7,61 @@ use serde::{Deserialize, Serialize};
use crate::observability::lifecycle as obs; use crate::observability::lifecycle as obs;
use crate::provisioning::{self, ProvisionLogStream}; use crate::provisioning::{self, ProvisionLogStream};
/// Structured Myelin lifecycle facts: run, node, stage, edge, ring, object, step, and worker events. /// Structured MVP lifecycle facts: run, node, stage, edge, ring, object, step, and worker events.
pub(crate) const MYELIN_LIFECYCLE: &str = "myelin.lifecycle"; pub(crate) const MVP_LIFECYCLE: &str = "mvp.lifecycle";
/// Structured node provisioning milestones emitted before a remote swactor runtime is live. /// Structured node provisioning milestones emitted before a remote swactor runtime is live.
pub(crate) const MYELIN_PROVISIONING_EVENTS: &str = "myelin.provisioning.events"; pub(crate) const MVP_PROVISIONING_EVENTS: &str = "mvp.provisioning.events";
/// Raw provider/process stream lines captured during provisioning. /// Raw provider/process stream lines captured during provisioning.
pub(crate) const MYELIN_PROVISIONING_LOGS: &str = "myelin.provisioning.logs"; pub(crate) const MVP_PROVISIONING_LOGS: &str = "mvp.provisioning.logs";
/// Datastream payload for the Myelin lifecycle channel. /// Datastream payload for the MVP lifecycle channel.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct MyelinLifecycleRecord { pub(crate) struct MvpLifecycleRecord {
pub event: obs::Event, pub event: obs::Event,
} }
impl Record for MyelinLifecycleRecord { impl Record for MvpLifecycleRecord {
const CHANNEL: &'static str = MYELIN_LIFECYCLE; const CHANNEL: &'static str = MVP_LIFECYCLE;
} }
/// Datastream payload for provisioning lifecycle events. /// Datastream payload for provisioning lifecycle events.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct MyelinProvisionEventRecord { pub(crate) struct MvpProvisionEventRecord {
pub event: provisioning::ProvisionEvent, pub event: provisioning::ProvisionEvent,
} }
impl MyelinProvisionEventRecord { impl MvpProvisionEventRecord {
pub(crate) fn new(event: provisioning::ProvisionEvent) -> Self { pub(crate) fn new(event: provisioning::ProvisionEvent) -> Self {
Self { event } Self { event }
} }
} }
impl Record for MyelinProvisionEventRecord { impl Record for MvpProvisionEventRecord {
const CHANNEL: &'static str = MYELIN_PROVISIONING_EVENTS; const CHANNEL: &'static str = MVP_PROVISIONING_EVENTS;
} }
/// Datastream payload for provisioning stdout/stderr/provider lines. /// Datastream payload for provisioning stdout/stderr/provider lines.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct MyelinProvisionLogRecord { pub(crate) struct MvpProvisionLogRecord {
pub line: provisioning::ProvisionLogLine, pub line: provisioning::ProvisionLogLine,
} }
impl MyelinProvisionLogRecord { impl MvpProvisionLogRecord {
pub(crate) fn new(line: provisioning::ProvisionLogLine) -> Self { pub(crate) fn new(line: provisioning::ProvisionLogLine) -> Self {
Self { line } Self { line }
} }
} }
pub(crate) fn myelin_provision_log_channel(node_id: u64, stream: ProvisionLogStream) -> String { pub(crate) fn mvp_provision_log_channel(node_id: u64, stream: ProvisionLogStream) -> String {
let stream = match stream { let stream = match stream {
ProvisionLogStream::Stdout => "stdout", ProvisionLogStream::Stdout => "stdout",
ProvisionLogStream::Stderr => "stderr", ProvisionLogStream::Stderr => "stderr",
ProvisionLogStream::Provider => "provider", ProvisionLogStream::Provider => "provider",
}; };
format!("myelin.provisioning.logs.node.{node_id}.{stream}") format!("mvp.provisioning.logs.node.{node_id}.{stream}")
} }
impl Record for MyelinProvisionLogRecord { impl Record for MvpProvisionLogRecord {
const CHANNEL: &'static str = MYELIN_PROVISIONING_LOGS; const CHANNEL: &'static str = MVP_PROVISIONING_LOGS;
} }

View file

@ -68,13 +68,6 @@ pub(crate) enum OrchestratorMsg {
stage_index: u32, stage_index: u32,
}, },
ObserveTokenEndpointsStopped, ObserveTokenEndpointsStopped,
ObserveOperatorStop {
run_id: u64,
},
ObserveMembershipLost {
run_id: u64,
node_id: u64,
},
AdvanceTimeMs(u64), AdvanceTimeMs(u64),
Snapshot { Snapshot {
reply_to: ActorAddress, reply_to: ActorAddress,
@ -83,7 +76,7 @@ pub(crate) enum OrchestratorMsg {
impl NetworkMessage for OrchestratorMsg { impl NetworkMessage for OrchestratorMsg {
fn type_tag() -> &'static str { fn type_tag() -> &'static str {
"myelin::OrchestratorMsg" "mvp_system::OrchestratorMsg"
} }
} }
@ -138,6 +131,7 @@ pub(crate) enum RunCommandWire {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum LifecycleEventWire { pub(crate) enum LifecycleEventWire {
RunRejected { run_id: u64 },
RunFaulted { run_id: u64 }, RunFaulted { run_id: u64 },
RunCompleted { run_id: u64 }, RunCompleted { run_id: u64 },
RunOperatorStopped { run_id: u64 }, RunOperatorStopped { run_id: u64 },
@ -186,7 +180,7 @@ pub(crate) enum OrchestratorReport {
impl NetworkMessage for OrchestratorReport { impl NetworkMessage for OrchestratorReport {
fn type_tag() -> &'static str { fn type_tag() -> &'static str {
"myelin::OrchestratorReport" "mvp_system::OrchestratorReport"
} }
} }
@ -278,17 +272,6 @@ impl OrchestratorActor {
OrchestratorMsg::ObserveTokenEndpointsStopped => { OrchestratorMsg::ObserveTokenEndpointsStopped => {
self.core.observe(core::RunEvent::TokenEndpointsStopped) self.core.observe(core::RunEvent::TokenEndpointsStopped)
} }
OrchestratorMsg::ObserveOperatorStop { run_id } => {
self.core.observe(core::RunEvent::OperatorStop {
run_id: core::RunId(run_id),
});
}
OrchestratorMsg::ObserveMembershipLost { run_id, node_id } => {
self.core.observe(core::RunEvent::MembershipLost {
run_id: core::RunId(run_id),
node_id: core::NodeId(node_id),
});
}
OrchestratorMsg::AdvanceTimeMs(delta) => self.core.advance_time_ms(delta), OrchestratorMsg::AdvanceTimeMs(delta) => self.core.advance_time_ms(delta),
} }
} }
@ -466,6 +449,9 @@ impl From<&core::RunCommand> for RunCommandWire {
impl From<&core::LifecycleEvent> for LifecycleEventWire { impl From<&core::LifecycleEvent> for LifecycleEventWire {
fn from(event: &core::LifecycleEvent) -> Self { fn from(event: &core::LifecycleEvent) -> Self {
match event { match event {
core::LifecycleEvent::RunRejected { run_id, .. } => {
Self::RunRejected { run_id: run_id.0 }
}
core::LifecycleEvent::RunFaulted { run_id, .. } => { core::LifecycleEvent::RunFaulted { run_id, .. } => {
Self::RunFaulted { run_id: run_id.0 } Self::RunFaulted { run_id: run_id.0 }
} }

View file

@ -88,7 +88,7 @@ pub(crate) fn looks_remote_image(image: &str) -> bool {
} }
/// Shared overlay for legacy and multi-binary configuration parsing. This accepts /// Shared overlay for legacy and multi-binary configuration parsing. This accepts
/// fields outside the fixed `myelin-chat` public config surface; `myelin-chat` uses a /// fields outside the fixed `mvp-chat` public config surface; `mvp-chat` uses a
/// bin-local strict config loader instead. /// bin-local strict config loader instead.
#[derive(Clone, Debug, Default, Deserialize, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(default)] #[serde(default)]

View file

@ -1,5 +1,5 @@
//! Myelin-system swactor distribution runtime wiring. //! MVP-system swactor distribution runtime wiring.
//! //!
//! This is the production version of the actor-stack setup that integration //! This is the production version of the actor-stack setup that integration
//! tests used to copy by hand: a swactor runtime, the four distribution protocol //! tests used to copy by hand: a swactor runtime, the four distribution protocol
@ -13,7 +13,6 @@ use std::time::{Duration, Instant};
use swactor::actor::{ActorAddress, ActorInterface}; use swactor::actor::{ActorAddress, ActorInterface};
use swactor::config::RuntimeConfig; use swactor::config::RuntimeConfig;
use swactor::runtime::{Ctx, Runtime}; use swactor::runtime::{Ctx, Runtime};
use swactor::stats::StatsHook;
use swactor::std::StdExtension; use swactor::std::StdExtension;
use swactor_transport::{CodecRegistry, CodecRemoteSink, NetworkMessage, TransportRouter}; use swactor_transport::{CodecRegistry, CodecRemoteSink, NetworkMessage, TransportRouter};
@ -45,6 +44,7 @@ pub(crate) struct DistributionActorAddrs {
} }
pub(crate) struct DistributionRuntimeStack { pub(crate) struct DistributionRuntimeStack {
pub node_id: NodeId,
pub runtime: Arc<Runtime>, pub runtime: Arc<Runtime>,
pub codec: Arc<CodecRegistry>, pub codec: Arc<CodecRegistry>,
pub outbox: Outbox, pub outbox: Outbox,
@ -61,7 +61,6 @@ impl DistributionRuntimeStack {
node_id: NodeId, node_id: NodeId,
config: DistributedNodeConfig, config: DistributedNodeConfig,
extend_codecs: impl FnOnce(&mut CodecRegistry), extend_codecs: impl FnOnce(&mut CodecRegistry),
stats_hook: Option<Arc<dyn StatsHook>>,
) -> Self { ) -> Self {
let mut runtime = let mut runtime =
Runtime::new(RuntimeConfig::default()).with_extension(Arc::new(StdExtension::new())); Runtime::new(RuntimeConfig::default()).with_extension(Arc::new(StdExtension::new()));
@ -73,9 +72,6 @@ impl DistributionRuntimeStack {
Arc::clone(&codec), Arc::clone(&codec),
Arc::clone(&transport_router), Arc::clone(&transport_router),
))); )));
if let Some(hook) = stats_hook {
runtime.set_stats_hook(hook);
}
let runtime = Arc::new(runtime); let runtime = Arc::new(runtime);
let outbox: Outbox = Arc::new(Mutex::new(Vec::new())); let outbox: Outbox = Arc::new(Mutex::new(Vec::new()));
@ -150,6 +146,7 @@ impl DistributionRuntimeStack {
.expect("subscribe MembershipFanout"); .expect("subscribe MembershipFanout");
Self { Self {
node_id,
runtime, runtime,
codec, codec,
outbox, outbox,

View file

@ -1,4 +1,4 @@
//! Myelin run orchestration public surface. //! MVP run orchestration public surface.
//! //!
//! This module owns run planning, membership/readiness coordination, deployment //! This module owns run planning, membership/readiness coordination, deployment
//! dispatch, run authority, prompt admission, and teardown behavior. Provider //! dispatch, run authority, prompt admission, and teardown behavior. Provider

View file

@ -1,6 +1,6 @@
//! Myelin-facing compatibility surface for reusable provider-neutral node provisioning. //! MVP-facing compatibility surface for reusable provider-neutral node provisioning.
//! //!
//! The reusable crate keeps `ProviderKind` opaque. Myelin provider selection policy //! The reusable crate keeps `ProviderKind` opaque. MVP provider selection policy
//! lives here so the reusable contracts do not know about process, Docker, or //! lives here so the reusable contracts do not know about process, Docker, or
//! VastAI runtime choices. //! VastAI runtime choices.

View file

@ -1,5 +1,5 @@
//! Relay provisioning shims for Myelin runtimes. //! Relay provisioning shims for MVP runtimes.
//! //!
//! The current implementations are deliberately small: local tests get the same //! The current implementations are deliberately small: local tests get the same
//! abstraction without an external relay, and deploy runs can point every node at //! abstraction without an external relay, and deploy runs can point every node at
@ -9,8 +9,8 @@
use iroh::{RelayMode, RelayUrl}; use iroh::{RelayMode, RelayUrl};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub(crate) const MYELIN_IROH_RELAY_MODE_ENV: &str = "MYELIN_IROH_RELAY_MODE"; pub(crate) const MVP_IROH_RELAY_MODE_ENV: &str = "MVP_IROH_RELAY_MODE";
pub(crate) const MYELIN_IROH_RELAY_URL_ENV: &str = "MYELIN_IROH_RELAY_URL"; pub(crate) const MVP_IROH_RELAY_URL_ENV: &str = "MVP_IROH_RELAY_URL";
pub(crate) const SWACTOR_IROH_RELAY_URL_ENV: &str = "SWACTOR_IROH_RELAY_URL"; pub(crate) const SWACTOR_IROH_RELAY_URL_ENV: &str = "SWACTOR_IROH_RELAY_URL";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -72,6 +72,12 @@ impl StaticRelayProvider {
parse_relay_url(raw).map(Self::new) parse_relay_url(raw).map(Self::new)
} }
pub(crate) fn from_env() -> Result<Option<Self>, String> {
selected_relay_url_from_env()
.map(|url| Self::from_url_str(&url).map(Some))
.unwrap_or(Ok(None))
}
pub(crate) fn url(&self) -> String { pub(crate) fn url(&self) -> String {
self.url.to_string() self.url.to_string()
} }
@ -102,7 +108,7 @@ impl RelayProvider for StaticRelayProvider {
} }
pub(crate) fn relay_runtime_config_from_env(run_id: u64) -> Result<RelayRuntimeConfig, String> { pub(crate) fn relay_runtime_config_from_env(run_id: u64) -> Result<RelayRuntimeConfig, String> {
let mode = env_optional(MYELIN_IROH_RELAY_MODE_ENV).map(|value| value.to_ascii_lowercase()); let mode = env_optional(MVP_IROH_RELAY_MODE_ENV).map(|value| value.to_ascii_lowercase());
let url = selected_relay_url_from_env(); let url = selected_relay_url_from_env();
relay_runtime_config_from_settings(run_id, mode.as_deref(), url.as_deref()) relay_runtime_config_from_settings(run_id, mode.as_deref(), url.as_deref())
} }
@ -119,7 +125,7 @@ pub(crate) fn relay_runtime_config_from_settings(
}), }),
None | Some("default") => relay_runtime_config_from_optional_static_provider(run_id, url), None | Some("default") => relay_runtime_config_from_optional_static_provider(run_id, url),
Some(other) => Err(format!( Some(other) => Err(format!(
"unsupported {MYELIN_IROH_RELAY_MODE_ENV}={other:?}; use disabled or default" "unsupported {MVP_IROH_RELAY_MODE_ENV}={other:?}; use disabled or default"
)), )),
} }
} }
@ -132,7 +138,7 @@ pub(crate) fn relay_mode_env_value(mode: &RelayMode) -> &'static str {
} }
pub(crate) fn selected_relay_url_from_env() -> Option<String> { pub(crate) fn selected_relay_url_from_env() -> Option<String> {
env_optional(MYELIN_IROH_RELAY_URL_ENV).or_else(|| env_optional(SWACTOR_IROH_RELAY_URL_ENV)) env_optional(MVP_IROH_RELAY_URL_ENV).or_else(|| env_optional(SWACTOR_IROH_RELAY_URL_ENV))
} }
fn relay_runtime_config_from_optional_static_provider( fn relay_runtime_config_from_optional_static_provider(

View file

@ -4,13 +4,12 @@ use std::io::{BufRead, BufReader, Read};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};
use std::sync::{Arc, mpsc}; use std::sync::{Arc, mpsc};
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle}; use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use datastream::DatastreamProducer; use datastream::DatastreamProducer;
use swactor::actor::{ActorAddress, ActorInterface}; use swactor::actor::{ActorAddress, ActorInterface};
use swactor::runtime::{Ctx, ExternalSender, Runtime, RuntimeConfig}; use swactor::runtime::{Ctx, ExternalSender, Runtime, RuntimeConfig, RuntimeHandle};
use swactor_vastai::{ use swactor_vastai::{
CreateInstanceRequest, LifecyclePolicy, Offer, ProvisionRequest, ProvisionedInstance, CreateInstanceRequest, LifecyclePolicy, Offer, ProvisionRequest, ProvisionedInstance,
SelectionPolicy, classify_vastai_error, create_instance, SelectionPolicy, classify_vastai_error, create_instance,
@ -36,7 +35,7 @@ pub(crate) struct VastAiProvisioningConfig {
impl Default for VastAiProvisioningConfig { impl Default for VastAiProvisioningConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
label_prefix: "myelin".to_owned(), label_prefix: "mvp".to_owned(),
disk_gb: 50, disk_gb: 50,
ssh_user: "root".to_owned(), ssh_user: "root".to_owned(),
selection: SelectionPolicy::default(), selection: SelectionPolicy::default(),
@ -56,46 +55,27 @@ pub(crate) struct VastAiSshEndpoint {
} }
pub(crate) struct VastAiProviderMonitor { pub(crate) struct VastAiProviderMonitor {
runtime: Arc<Runtime>, runtime: Option<RuntimeHandle>,
actor: ActorAddress, actor: ActorAddress,
tick_thread: Option<JoinHandle<()>>,
stop_flag: Arc<AtomicBool>,
} }
impl VastAiProviderMonitor { impl VastAiProviderMonitor {
fn new(runtime: Runtime, actor: ActorAddress) -> Self { fn new(runtime: RuntimeHandle, actor: ActorAddress) -> Self {
let runtime = Arc::new(runtime);
let stop_flag = Arc::new(AtomicBool::new(false));
let rt = Arc::clone(&runtime);
let flag = Arc::clone(&stop_flag);
let tick_thread = thread::spawn(move || {
while !flag.load(Ordering::Relaxed) || rt.has_work() {
if rt.has_work() {
rt.tick();
} else {
thread::sleep(Duration::from_millis(10));
}
}
});
Self { Self {
runtime, runtime: Some(runtime),
actor, actor,
tick_thread: Some(tick_thread),
stop_flag,
} }
} }
fn stop(&mut self) { fn stop(&mut self) {
let Some(tick_thread) = self.tick_thread.take() else { let Some(runtime) = self.runtime.take() else {
return; return;
}; };
let _ = self let _ = runtime
.runtime .runtime
.send_to(self.actor, VastAiProviderMonitorMsg::Stop); .send_to(self.actor, VastAiProviderMonitorMsg::Stop);
self.stop_flag.store(true, Ordering::Relaxed); runtime.shutdown();
let _ = tick_thread.join(); runtime.join();
} }
} }
@ -572,6 +552,7 @@ impl VastAiLeaseClient for ToolsVastAiLeaseClient {
sender, sender,
)) ))
.ok()?; .ok()?;
let runtime = runtime.run().ok()?;
Some(VastAiProviderMonitor::new(runtime, actor)) Some(VastAiProviderMonitor::new(runtime, actor))
} }

View file

@ -1,7 +1,7 @@
//! Myelin-local provisioning provider plugins. //! MVP-local provisioning provider plugins.
//! //!
//! Provider-neutral lifecycle contracts live in the reusable `provisioning` //! Provider-neutral lifecycle contracts live in the reusable `provisioning`
//! crate. This module keeps Myelin-local process/Docker plugin implementations //! crate. This module keeps MVP-local process/Docker plugin implementations
//! that know about bootstrap datastream plumbing and local runtime execution. //! that know about bootstrap datastream plumbing and local runtime execution.
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -118,7 +118,7 @@ fn prepare_docker_file_volume(
&["volume", "create", &volume], &["volume", "create", &volume],
"create cached model docker volume", "create cached model docker volume",
)?; )?;
let loader_name = format!("myelin-cache-load-{}-{volume}", std::process::id()); let loader_name = format!("mvp-cache-load-{}-{volume}", std::process::id());
let _ = Command::new("docker") let _ = Command::new("docker")
.args(["rm", "-f", &loader_name]) .args(["rm", "-f", &loader_name])
.stdout(Stdio::null()) .stdout(Stdio::null())
@ -173,7 +173,7 @@ fn docker_file_cache_volume_name(path: &Path) -> Result<String, String> {
.and_then(|name| name.to_str()) .and_then(|name| name.to_str())
.map(safe_docker_volume_component) .map(safe_docker_volume_component)
.unwrap_or_else(|| "model".to_owned()); .unwrap_or_else(|| "model".to_owned());
Ok(format!("myelin-cache-{file}-{}-{modified}", metadata.len())) Ok(format!("mvp-cache-{file}-{}-{modified}", metadata.len()))
} }
fn safe_docker_volume_component(value: &str) -> String { fn safe_docker_volume_component(value: &str) -> String {
@ -455,14 +455,14 @@ impl ProvisionPlugin for LocalDockerPlugin {
let docker_gpus = spec let docker_gpus = spec
.env .env
.iter() .iter()
.find(|(key, _)| key == "MYELIN_DOCKER_GPUS") .find(|(key, _)| key == "MVP_DOCKER_GPUS")
.map(|(_, value)| value.clone()) .map(|(_, value)| value.clone())
.or_else(|| std::env::var("MYELIN_DOCKER_GPUS").ok()) .or_else(|| std::env::var("MVP_DOCKER_GPUS").ok())
.filter(|value| !value.trim().is_empty()); .filter(|value| !value.trim().is_empty());
sink.observe(PluginObservation::DatastreamFrame { sink.observe(PluginObservation::DatastreamFrame {
run_id: spec.run_id, run_id: spec.run_id,
node_id: spec.node_id, node_id: spec.node_id,
channel: "myelin.provisioning.events".to_owned(), channel: "mvp.node.bootstrap".to_owned(),
payload: serde_json::json!({ payload: serde_json::json!({
"type":"DockerGpuConfigResolved", "type":"DockerGpuConfigResolved",
"provider":"Docker", "provider":"Docker",

View file

@ -16,6 +16,16 @@ pub(crate) struct RunPlan {
pub stages: Vec<StageRef>, pub stages: Vec<StageRef>,
} }
impl RunPlan {
pub(crate) fn test_linear(run_id: RunId, stages: Vec<StageRef>) -> Self {
Self { run_id, stages }
}
pub(crate) fn stage_nodes(&self) -> Vec<NodeId> {
self.stages.iter().map(|stage| stage.node_id).collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct RunConfig { pub(crate) struct RunConfig {
pub run_id: RunId, pub run_id: RunId,
@ -115,6 +125,10 @@ pub(crate) enum RunFaultReason {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum LifecycleEvent { pub(crate) enum LifecycleEvent {
RunRejected {
run_id: RunId,
reason: RunFaultReason,
},
RunFaulted { RunFaulted {
run_id: RunId, run_id: RunId,
reason: RunFaultReason, reason: RunFaultReason,
@ -161,6 +175,8 @@ pub(crate) enum RunCommand {
}, },
} }
pub type OrchestratorHarness = OrchestratorRun;
pub(crate) struct OrchestratorRun { pub(crate) struct OrchestratorRun {
config: RunConfig, config: RunConfig,
plan: Option<RunPlan>, plan: Option<RunPlan>,

View file

@ -165,6 +165,7 @@ pub(crate) enum EdgeKind {
pub(crate) enum ObjectKind { pub(crate) enum ObjectKind {
Token, Token,
Activation, Activation,
Weight,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]

View file

@ -0,0 +1,3 @@
//! MVP prompt protocol public surface.
pub(crate) mod rpc;

View file

@ -56,7 +56,7 @@ impl PromptEvent {
impl NetworkMessage for PromptEvent { impl NetworkMessage for PromptEvent {
fn type_tag() -> &'static str { fn type_tag() -> &'static str {
"myelin::PromptEvent" "mvp_system::PromptEvent"
} }
} }
@ -69,7 +69,7 @@ pub(crate) enum TokenizerEvent {
impl NetworkMessage for TokenizerEvent { impl NetworkMessage for TokenizerEvent {
fn type_tag() -> &'static str { fn type_tag() -> &'static str {
"myelin::TokenizerEvent" "mvp_system::TokenizerEvent"
} }
} }

View file

@ -19,6 +19,12 @@ pub(crate) struct DeviceHandle {
pub id: u64, pub id: u64,
} }
impl DeviceHandle {
pub(crate) fn new_current(id: u64) -> Self {
Self { generation: 1, id }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct LayerRange { pub(crate) struct LayerRange {
pub start: u32, pub start: u32,
@ -44,6 +50,14 @@ impl WeightSource {
tokenizer, tokenizer,
} }
} }
pub(crate) fn embedded_gguf(model_id: impl Into<String>, path: impl Into<String>) -> Self {
Self::new(
model_id,
GgufSource::LocalPath(path.into()),
TokenizerSource::EmbeddedGguf,
)
}
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -214,6 +228,9 @@ pub(crate) enum StageCommand {
range: LayerRange, range: LayerRange,
shard_plan: Option<StageShardPlan>, shard_plan: Option<StageShardPlan>,
}, },
RewireEdge {
edge_id: EdgeId,
},
ExecuteStep(ExecuteStep), ExecuteStep(ExecuteStep),
ReleaseInputHandle { ReleaseInputHandle {
object_id: ObjectId, object_id: ObjectId,
@ -227,6 +244,8 @@ pub(crate) enum StageCommand {
}, },
} }
pub type StageControllerHarness = StageController;
pub(crate) struct StageController { pub(crate) struct StageController {
local_node_id: NodeId, local_node_id: NodeId,
provision: Option<ProvisionStage>, provision: Option<ProvisionStage>,

View file

@ -35,7 +35,7 @@ impl GgufPlanningMetadata {
.map(u64::from) .map(u64::from)
.unwrap_or(DEFAULT_EFFECTIVE_CONTEXT); .unwrap_or(DEFAULT_EFFECTIVE_CONTEXT);
if requested_context == 0 { if requested_context == 0 {
return Err("MYELIN_MAX_CONTEXT/--max-context must be greater than 0".to_owned()); return Err("MVP_MAX_CONTEXT/--max-context must be greater than 0".to_owned());
} }
let effective_context = requested_context.min(self.context_length); let effective_context = requested_context.min(self.context_length);
if effective_context == 0 { if effective_context == 0 {

View file

@ -728,7 +728,7 @@ fn fetch_http_range(url: &str, start: u64, len: u64) -> Result<Vec<u8>, String>
let range = format!("bytes={start}-{end}"); let range = format!("bytes={start}-{end}");
let mut request = ureq::get(url) let mut request = ureq::get(url)
.set("Range", &range) .set("Range", &range)
.set("User-Agent", "myelin-node/0.1"); .set("User-Agent", "swactor-mvp-node/0.1");
if let Ok(token) = std::env::var("HF_TOKEN") if let Ok(token) = std::env::var("HF_TOKEN")
&& !token.trim().is_empty() && !token.trim().is_empty()
{ {

View file

@ -1,5 +1,5 @@
//! Myelin stage control, shard planning, and weight lifecycle public surface. //! MVP stage control, shard planning, and weight lifecycle public surface.
pub(crate) mod control; pub(crate) mod control;
pub(crate) mod gguf_metadata; pub(crate) mod gguf_metadata;

View file

@ -1,12 +1,12 @@
//! Local end-to-end behavior guarantee for the Myelin system lifecycle. //! Local end-to-end behavior guarantee for the MVP system lifecycle.
//! //!
//! This test composes the crate's Myelin contract harnesses through one in-process //! This test composes the crate's MVP contract harnesses through one in-process
//! mock environment. It deliberately avoids Docker, real SWIM, real iroh, GGUF, //! mock environment. It deliberately avoids Docker, real SWIM, real iroh, GGUF,
//! and CUDA while still driving the run through planning, provisioning, //! and CUDA while still driving the run through planning, provisioning,
//! readiness, prompt injection, stage execution, completion, and teardown. //! readiness, prompt injection, stage execution, completion, and teardown.
use myelin::observability::lifecycle as obs; use mvp_system::observability::lifecycle as obs;
use myelin::orchestration::engine_builder as engine; use mvp_system::orchestration::engine_builder as engine;
use super::local_mock::{ use super::local_mock::{
LocalMockCluster, LocalMockConfig, LocalMockOutcome, assert_happy_path_lifecycle, LocalMockCluster, LocalMockConfig, LocalMockOutcome, assert_happy_path_lifecycle,

View file

@ -1,4 +1,4 @@
use myelin::observability::lifecycle as obs; use mvp_system::observability::lifecycle as obs;
use super::mock_transport::MockObjectKind; use super::mock_transport::MockObjectKind;

View file

@ -2,11 +2,10 @@ use std::collections::{BTreeMap, BTreeSet};
use crate::run_fsm as fsm; use crate::run_fsm as fsm;
use crate::run_plan as plan; use crate::run_plan as plan;
use crate::tests::harness::OrchestratorHarness;
use data_plane::edge_actor; use data_plane::edge_actor;
use myelin::observability::lifecycle as obs; use mvp_system::observability::lifecycle as obs;
use myelin::orchestration::engine_builder as engine; use mvp_system::orchestration::engine_builder as engine;
use myelin::staging as stage; use mvp_system::staging as stage;
use super::mock_node::MockNode; use super::mock_node::MockNode;
use super::mock_transport::{Delivery, MockObject, MockObjectKind, MockTransport}; use super::mock_transport::{Delivery, MockObject, MockObjectKind, MockTransport};
@ -35,7 +34,7 @@ pub struct LocalMockCluster {
engine_events: Vec<engine::EngineEvent>, engine_events: Vec<engine::EngineEvent>,
plan: plan::RunPlan, plan: plan::RunPlan,
nodes: BTreeMap<u32, MockNode>, nodes: BTreeMap<u32, MockNode>,
orchestrator: Option<OrchestratorHarness>, orchestrator: Option<fsm::OrchestratorHarness>,
orchestrator_command_cursor: usize, orchestrator_command_cursor: usize,
orchestrator_event_cursor: usize, orchestrator_event_cursor: usize,
trace: Vec<obs::Event>, trace: Vec<obs::Event>,
@ -304,7 +303,7 @@ impl LocalMockCluster {
self.orchestrator_command_cursor = 0; self.orchestrator_command_cursor = 0;
self.orchestrator_event_cursor = 0; self.orchestrator_event_cursor = 0;
self.scenario = scenario; self.scenario = scenario;
self.orchestrator = Some(OrchestratorHarness::new(fsm::RunConfig { self.orchestrator = Some(fsm::OrchestratorHarness::new(fsm::RunConfig {
run_id: fsm::RunId(self.run_id.0), run_id: fsm::RunId(self.run_id.0),
max_tokens: u64::from(self.max_tokens), max_tokens: u64::from(self.max_tokens),
prompt: tokenize(prompt), prompt: tokenize(prompt),
@ -743,6 +742,7 @@ impl LocalMockCluster {
self.push_run(obs::EventKind::RunCompleted, obs::Component::Orchestrator); self.push_run(obs::EventKind::RunCompleted, obs::Component::Orchestrator);
} }
fsm::LifecycleEvent::RunFaulted { .. } fsm::LifecycleEvent::RunFaulted { .. }
| fsm::LifecycleEvent::RunRejected { .. }
| fsm::LifecycleEvent::RunOperatorStopped { .. } => { | fsm::LifecycleEvent::RunOperatorStopped { .. } => {
self.push_run(obs::EventKind::RunFaulted, obs::Component::Orchestrator); self.push_run(obs::EventKind::RunFaulted, obs::Component::Orchestrator);
} }
@ -852,7 +852,7 @@ impl LocalMockCluster {
.count() .count()
} }
fn orchestrator_mut(&mut self) -> &mut OrchestratorHarness { fn orchestrator_mut(&mut self) -> &mut fsm::OrchestratorHarness {
self.orchestrator self.orchestrator
.as_mut() .as_mut()
.expect("run_prompt must initialize orchestrator") .expect("run_prompt must initialize orchestrator")

View file

@ -1,7 +1,6 @@
use crate::run_plan as plan; use crate::run_plan as plan;
use data_plane::edge_actor; use data_plane::edge_actor;
use myelin::staging as stage; use mvp_system::staging as stage;
use crate::tests::harness::StageControllerHarness;
use super::mock_transport::MockObject; use super::mock_transport::MockObject;
use super::mock_worker::MockWorker; use super::mock_worker::MockWorker;
@ -17,7 +16,7 @@ pub struct MockNode {
inbound_edge: Option<plan::EdgeId>, inbound_edge: Option<plan::EdgeId>,
outbound_edge: Option<plan::EdgeId>, outbound_edge: Option<plan::EdgeId>,
outbound_object_allocator: Option<edge_actor::ObjectIdAllocator>, outbound_object_allocator: Option<edge_actor::ObjectIdAllocator>,
controller: StageControllerHarness, controller: stage::StageControllerHarness,
worker: MockWorker, worker: MockWorker,
event_cursor: usize, event_cursor: usize,
} }
@ -35,7 +34,7 @@ impl MockNode {
inbound_edge: None, inbound_edge: None,
outbound_edge: None, outbound_edge: None,
outbound_object_allocator: None, outbound_object_allocator: None,
controller: StageControllerHarness::new(stage::NodeId(node_id.0)), controller: stage::StageControllerHarness::new(stage::NodeId(node_id.0)),
worker: MockWorker::new(stage_index, eos_after_sequence), worker: MockWorker::new(stage_index, eos_after_sequence),
event_cursor: 0, event_cursor: 0,
} }

View file

@ -1,5 +1,5 @@
use crate::run_plan as plan; use crate::run_plan as plan;
use myelin::staging as stage; use mvp_system::staging as stage;
use super::mock_transport::{MockObject, MockObjectKind}; use super::mock_transport::{MockObject, MockObjectKind};

View file

@ -1,4 +1,3 @@
mod harness;
mod local_e2e_guarantees; mod local_e2e_guarantees;
mod local_mock; mod local_mock;
mod node_guarantees; mod node_guarantees;

View file

@ -3,7 +3,7 @@
use crate::node_actor::{NodeAgentActor, NodeAgentMsg, NodeAgentReport}; use crate::node_actor::{NodeAgentActor, NodeAgentMsg, NodeAgentReport};
use crate::orchestration::actor::OrchestratorMsg; use crate::orchestration::actor::OrchestratorMsg;
use iroh::{EndpointAddr, SecretKey}; use iroh::{EndpointAddr, SecretKey};
use myelin::staging as stage; use mvp_system::staging as stage;
use swactor::actor::ActorAddress; use swactor::actor::ActorAddress;
use swactor::config::RuntimeConfig; use swactor::config::RuntimeConfig;
use swactor::runtime::Runtime; use swactor::runtime::Runtime;

View file

@ -1,6 +1,6 @@
//! Behavior guarantees for the `observability` module. //! Behavior guarantees for the `observability` module.
//! Black-box contract tests for the Myelin observability surface. //! Black-box contract tests for the MVP observability surface.
//! //!
//! These tests intentionally know only the public event stream surface: //! These tests intentionally know only the public event stream surface:
//! //!
@ -8,9 +8,9 @@
//! - event identities, reason enums, and ordering observed by subscribers //! - event identities, reason enums, and ordering observed by subscribers
//! //!
//! They assert the guarantees in //! They assert the guarantees in
//! `specs/myelin_system/observability_surface_contract.md`. //! `specs/mvp_system/observability_surface_contract.md`.
use myelin::observability::lifecycle as obs; use mvp_system::observability::lifecycle as obs;
// The trace fixture contains one successful run from boot through teardown. // The trace fixture contains one successful run from boot through teardown.
// Tests use structured events only; logs, transport, storage, and batching stay // Tests use structured events only; logs, transport, storage, and batching stay

View file

@ -1,7 +1,7 @@
//! Behavior guarantees for the `orchestration` module. //! Behavior guarantees for the `orchestration` module.
mod run_plan { mod run_plan {
//! Black-box contract tests for Myelin RunPlan formation. //! Black-box contract tests for MVP RunPlan formation.
//! //!
//! These tests intentionally know only the public planning surface: //! These tests intentionally know only the public planning surface:
//! //!
@ -205,7 +205,7 @@ mod run_plan {
} }
} }
// This proves every edge has one public producer and one public consumer, and // This proves every edge has one public producer and one public consumer, and
// that the returned edge graph is exactly the Myelin linear pipeline. // that the returned edge graph is exactly the MVP linear pipeline.
#[test] #[test]
fn edge_graph_is_exactly_the_linear_pipeline() { fn edge_graph_is_exactly_the_linear_pipeline() {
// Use four stages so the activation chain has multiple interior edges. // Use four stages so the activation chain has multiple interior edges.
@ -474,7 +474,7 @@ mod run_plan {
input input
} }
// Zero stages cannot form the Myelin pipeline. This fixture isolates the invalid // Zero stages cannot form the MVP pipeline. This fixture isolates the invalid
// stage-count path without adding any other contradictory facts. // stage-count path without adding any other contradictory facts.
fn invalid_zero_stage_count() -> PlannerInput { fn invalid_zero_stage_count() -> PlannerInput {
valid_input(0, 36) valid_input(0, 36)
@ -491,7 +491,7 @@ mod run_plan {
} }
mod run_fsm { mod run_fsm {
//! Black-box contract tests for the Myelin orchestrator run FSM. //! Black-box contract tests for the MVP orchestrator run FSM.
//! //!
//! These tests intentionally know only the public orchestrator surface: //! These tests intentionally know only the public orchestrator surface:
//! //!
@ -502,7 +502,6 @@ mod run_fsm {
//! `specs/BEHAVIOR_GUARANTEES.md`. //! `specs/BEHAVIOR_GUARANTEES.md`.
use crate::run_fsm as fsm; use crate::run_fsm as fsm;
use crate::tests::harness::OrchestratorHarness;
// A three-stage plan proves multi-stage provisioning and readiness without // A three-stage plan proves multi-stage provisioning and readiness without
// making tests depend on any placement heuristic. The plan is already valid; // making tests depend on any placement heuristic. The plan is already valid;
@ -530,8 +529,8 @@ mod run_fsm {
// The harness is the black-box public boundary for the run FSM. It accepts // The harness is the black-box public boundary for the run FSM. It accepts
// observable events and records emitted commands/events; tests never inspect an // observable events and records emitted commands/events; tests never inspect an
// internal FSM enum or private readiness counter. // internal FSM enum or private readiness counter.
fn new_run() -> OrchestratorHarness { fn new_run() -> fsm::OrchestratorHarness {
OrchestratorHarness::new(fsm::RunConfig { fsm::OrchestratorHarness::new(fsm::RunConfig {
run_id: fsm::RunId(7), run_id: fsm::RunId(7),
max_tokens: 4, max_tokens: 4,
prompt: vec![101, 102, 103], prompt: vec![101, 102, 103],
@ -681,6 +680,7 @@ mod run_fsm {
}); });
assert!(invalid.events().iter().any(|event| { assert!(invalid.events().iter().any(|event| {
matches!(event, fsm::LifecycleEvent::RunFaulted { .. }) matches!(event, fsm::LifecycleEvent::RunFaulted { .. })
|| matches!(event, fsm::LifecycleEvent::RunRejected { .. })
})); }));
} }

View file

@ -1,6 +1,6 @@
//! Behavior guarantees for the `prompt` module. //! Behavior guarantees for the `prompt` module.
use myelin::prompt::rpc::{PromptEvent, SubmitPrompt}; use mvp_system::prompt::rpc::{PromptEvent, SubmitPrompt};
#[test] #[test]
fn zero_request_limits_take_loop_defaults() { fn zero_request_limits_take_loop_defaults() {

View file

@ -1,7 +1,7 @@
//! Behavior guarantees for the `staging` module. //! Behavior guarantees for the `staging` module.
mod stage_controller { mod stage_controller {
//! Black-box contract tests for Myelin StageController behavior. //! Black-box contract tests for MVP StageController behavior.
//! //!
//! These tests intentionally know only the public stage-controller surface: //! These tests intentionally know only the public stage-controller surface:
//! //!
@ -11,8 +11,7 @@ mod stage_controller {
//! They assert the guarantees in //! They assert the guarantees in
//! `specs/BEHAVIOR_GUARANTEES.md`. //! `specs/BEHAVIOR_GUARANTEES.md`.
use myelin::staging as stage; use mvp_system::staging as stage;
use crate::tests::harness::StageControllerHarness;
// This provision fixture represents a single middle stage. It has one inbound // This provision fixture represents a single middle stage. It has one inbound
// and one outbound edge so tests can prove the controller uses assigned edges // and one outbound edge so tests can prove the controller uses assigned edges
@ -38,8 +37,8 @@ mod stage_controller {
// The harness exposes only public messages. Tests intentionally do not inspect // The harness exposes only public messages. Tests intentionally do not inspect
// private controller states such as "Preparing" or "Executing"; they infer // private controller states such as "Preparing" or "Executing"; they infer
// controller behavior from emitted commands and lifecycle events. // controller behavior from emitted commands and lifecycle events.
fn new_controller() -> StageControllerHarness { fn new_controller() -> stage::StageControllerHarness {
StageControllerHarness::new(stage::NodeId(11)) stage::StageControllerHarness::new(stage::NodeId(11))
} }
// Preparation readiness has four independent prerequisites. Listing them as // Preparation readiness has four independent prerequisites. Listing them as
@ -61,7 +60,7 @@ mod stage_controller {
// This helper provisions and readies a stage through public events. Tests that // This helper provisions and readies a stage through public events. Tests that
// focus on execution use it to avoid duplicating setup while still going through // focus on execution use it to avoid duplicating setup while still going through
// the same observable path as production. // the same observable path as production.
fn ready_stage() -> StageControllerHarness { fn ready_stage() -> stage::StageControllerHarness {
let mut harness = new_controller(); let mut harness = new_controller();
harness.observe(stage::StageEvent::ProvisionStage { harness.observe(stage::StageEvent::ProvisionStage {
from: stage::NodeId(99), from: stage::NodeId(99),
@ -104,6 +103,15 @@ mod stage_controller {
) )
})); }));
// The controller must not emit any command that replaces the provisioned
// edge ids with a locally chosen edge.
assert!(
!harness
.commands()
.iter()
.any(|command| { matches!(command, stage::StageCommand::RewireEdge { .. }) })
);
// An unauthorized provision attempt must fault before setup can begin. // An unauthorized provision attempt must fault before setup can begin.
let mut unauthorized = new_controller(); let mut unauthorized = new_controller();
unauthorized.observe(stage::StageEvent::ProvisionStage { unauthorized.observe(stage::StageEvent::ProvisionStage {
@ -201,7 +209,7 @@ mod stage_controller {
assert_eq!(execute_steps[0].outputs[0].sequence, 0); assert_eq!(execute_steps[0].outputs[0].sequence, 0);
// A second object while the first step is active must not create another // A second object while the first step is active must not create another
// active ExecuteStep in the Myelin. // active ExecuteStep in the MVP.
harness.observe(stage::StageEvent::ObjectLoaded { harness.observe(stage::StageEvent::ObjectLoaded {
edge_id: stage::EdgeId(7001), edge_id: stage::EdgeId(7001),
object_id: stage::ObjectId(9001), object_id: stage::ObjectId(9001),
@ -511,7 +519,7 @@ mod stage_controller {
} }
mod weight_lifecycle { mod weight_lifecycle {
//! Black-box contract tests for Myelin stage-local weight lifecycle. //! Black-box contract tests for MVP stage-local weight lifecycle.
//! //!
//! These tests intentionally know only the public weight-work surface: //! These tests intentionally know only the public weight-work surface:
//! //!
@ -522,7 +530,7 @@ mod weight_lifecycle {
//! They assert the guarantees in //! They assert the guarantees in
//! `specs/BEHAVIOR_GUARANTEES.md`. //! `specs/BEHAVIOR_GUARANTEES.md`.
use myelin::staging::weight_lifecycle as weights; use mvp_system::staging::weight_lifecycle as weights;
// A valid assignment gives the stage exactly one layer range and one source. // A valid assignment gives the stage exactly one layer range and one source.
// Tests vary only source or failure outcome so the assignment contract remains // Tests vary only source or failure outcome so the assignment contract remains

Some files were not shown because too many files have changed in this diff Show more