Skip to content

tauri: bundle myownmesh daemon as sidecar (zero-install)#204

Merged
mrjeeves merged 8 commits into
mainfrom
claude/llm-on-myownmesh-engine
May 28, 2026
Merged

tauri: bundle myownmesh daemon as sidecar (zero-install)#204
mrjeeves merged 8 commits into
mainfrom
claude/llm-on-myownmesh-engine

Conversation

@mrjeeves
Copy link
Copy Markdown
Owner

Summary

PR #203 left the daemon spawn path dependent on myownmesh already being on the system — users installing the LLM via .dmg / .deb / .msi saw "couldn't find myownmesh" at first launch. Closing that gap: the daemon now bundles with the LLM as a Tauri sidecar, zero separate install.

How it works

src-tauri/build.rs builds (or copies) the daemon at LLM compile time. Resolution order:

  1. MYOWNLLM_MESH_BIN / MYOWNMESH_BIN env var — release CI brings a pre-signed binary.
  2. Sibling MyOwnMesh checkout at ../../MyOwnMesh/target/{debug,release}/ — dev convenience when both repos are open as siblings.
  3. cargo install --git --rev <pinned> — fetches the daemon source at the pinned rev, builds it, caches in OUT_DIR keyed by rev. Subsequent builds short-circuit.

The built binary lands at src-tauri/binaries/myownmesh-<target-triple>{.exe} (Tauri's externalBin convention). tauri.conf.json::bundle::externalBin declares it, so the bundler places it next to the main LLM exe in every shipped artifact (macOS Contents/MacOS/, Linux /usr/lib/MyOwnLLM/, Windows next to MyOwnLLM.exe).

.myownmesh-rev at the repo root pins the daemon revision — single source of truth, currently 93b53628 (the merged Phase A daemon-IPC commit).

find_daemon_binary now checks the sidecar location (<exe_dir>/myownmesh{.exe}) first. Env-var overrides + PATH + workspace fallbacks retained for dev / CI / offline paths.

Offline / dev escape hatches

  • MYOWNLLM_SKIP_SIDECAR=1build.rs writes a zero-byte stub at the sidecar slot so tauri_build::build()'s externalBin existence check passes; find_daemon_binary detects the stub via len() > 0 and falls through.
  • MYOWNLLM_MESH_BIN env var — point at a pre-built daemon (skips the fetch).
  • Sibling MyOwnMesh workspace — auto-detected, fastest dev path.

Validation

  • cargo check --bins with sibling-workspace path: copies the real 245 MB debug ELF into the sidecar slot, build clean.
  • cargo check --bins with MYOWNLLM_SKIP_SIDECAR=1: stub written, build clean.

Files

.gitignore                            | +5    (src-tauri/binaries/)
.myownmesh-rev                        | +1    (new — pinned daemon SHA)
src-tauri/build.rs                    | +200  (bundle logic + tauri_build call)
src-tauri/tauri.conf.json             | +3    (externalBin entry)
src-tauri/src/mesh/daemon.rs          | +30 / -22  (sidecar lookup first; stub detection)

Test plan

  • cargo check --bins against sibling path.
  • cargo check --bins against SKIP_SIDECAR stub fallback.
  • tauri build produces a .deb / .dmg / .msi with the daemon bundled next to the main exe (need release-CI validation).
  • First-launch on a clean install: daemon spawns from the sidecar slot without user action.

https://claude.ai/code/session_01Vp4cvRTaLYd3162EwwcCXg


Generated by Claude Code

The earlier daemon-spawn path required the `myownmesh` binary to
already be on `$PATH` or shipped via a separate install — users
who installed the LLM via `.dmg` / `.deb` / `.msi` got "couldn't
find myownmesh" at first launch. We control both repos; this
should Just Work.

Now it does:

**`src-tauri/build.rs`** — bundles the daemon as a Tauri sidecar
at LLM build time. Resolution order:
1. `MYOWNLLM_MESH_BIN` / `MYOWNMESH_BIN` env var pointing at a
   pre-built binary (used by release CI to bring in a pre-signed
   daemon).
2. Sibling MyOwnMesh checkout's `target/{debug,release}/` (dev
   convenience — both repos open as siblings).
3. `cargo install --git https://github.com/mrjeeves/MyOwnMesh
   --rev <pinned> myownmesh --root <OUT_DIR>` — fetches the
   pinned daemon source, builds it, caches in OUT_DIR keyed by
   rev. Subsequent builds short-circuit when the rev hasn't
   moved.

The pinned rev lives in `.myownmesh-rev` at the repo root —
single source of truth, matched against the `myownmesh-core`
Cargo.toml dep so daemon IPC wire shape stays in lockstep with
what `src/mesh/daemon.rs` expects. Current pin: 93b53628 (the
merged Phase A daemon-IPC PR).

The built binary lands at
`src-tauri/binaries/myownmesh-<target-triple>{.exe}` — Tauri's
`externalBin` convention. `tauri.conf.json` declares it under
`bundle.externalBin`, so the bundler copies it next to the main
LLM exe in every shipped artifact:
- macOS `.app`: `Contents/MacOS/myownmesh`
- Linux `.deb`: `/usr/lib/MyOwnLLM/myownmesh`
- Windows `.msi`: next to `MyOwnLLM.exe`

**`mesh/daemon.rs::find_daemon_binary`** — sidecar location
(`<exe_dir>/myownmesh{.exe}`) is now the first lookup. End users
never see a "couldn't find" error in the normal install flow.
Env-var overrides + PATH + workspace fallbacks kept for dev /
release-CI / offline-build paths.

**Offline / dev escape hatches**:
- `MYOWNLLM_SKIP_SIDECAR=1` — build.rs skips the daemon fetch
  + writes a zero-byte stub at the sidecar slot so
  `tauri_build::build()`'s `externalBin` existence check still
  passes. `find_daemon_binary` detects the zero-byte stub via a
  `metadata().len() > 0` check and falls through to the next
  resolution rule.
- `MYOWNLLM_MESH_BIN` / `MYOWNMESH_BIN` — point at a pre-built
  binary (skips the fetch entirely).
- Sibling MyOwnMesh checkout — auto-detected, fastest dev path.

**`.gitignore`**: `src-tauri/binaries/` (build artefact; the
pinned source rev tracked via `.myownmesh-rev` is the real
input).

Validation: `cargo check --bins` clean against both the
sibling-workspace path (real ELF binary copied) and the
SKIP path (zero-byte stub + runtime fallback). The
`find_daemon_binary` change keeps env / PATH / workspace
fallbacks compatible with everything that worked before.

https://claude.ai/code/session_01Vp4cvRTaLYd3162EwwcCXg
@mrjeeves mrjeeves force-pushed the claude/llm-on-myownmesh-engine branch from f11a754 to 2ba396d Compare May 27, 2026 21:21
claude added 7 commits May 27, 2026 21:34
Two issues your `just dev` hit:

**`cargo install` stderr was eaten** — `build.rs` ran `.status()`
which inherits stderr but cargo build buffers sub-process output
until the build itself fails, and our `Ok` return with a warning
swallowed it. You saw `exit code: 101` with no diagnostic. The
actual failure was probably libsrtp / cmake / openssl missing on
your Windows box — webrtc-rs needs them and CI installs them in
its setup step, but a fresh dev install doesn't.

Now build.rs captures stderr and surfaces the last 60 lines on
failure, so the next time `cargo install myownmesh` blows up
you'll see *what* blew up — almost always a missing native dep
that an install of cmake / vs-build-tools / openssl fixes.

Also dropped `--locked` from the cargo install args. The cloned
checkout's `Cargo.lock` may post-date the rev we pin, which
makes locked install refuse to start. Without it cargo resolves
against crates.io's latest compatible versions — same as what
`cargo build -p myownmesh` would do in a fresh checkout.

**`find_daemon_binary` returned a stale path that doesn't spawn**
— your log showed it resolving to
`C:\Users\Admin\Dev\MyOwnLLM\src-tauri\target\debug\myownmesh.exe`
(probably leftover from an earlier partial cargo install), then
`spawn` failed because the file isn't a valid PE binary. The old
single-binary lookup returned the first path that *existed* and
gave up if that one didn't actually work.

`daemon_binary_candidates()` now returns the full priority list
(sidecar → env vars → PATH → workspace fallbacks), de-duped by
canonical path. `ensure_daemon_running` iterates each — if one
fails to spawn or never binds the control socket, it drops the
child and tries the next. The error message at the end lists
every path that was tried plus the last failure, so when nothing
works the diagnostic is concrete.

This commit alone won't fix the `cargo install` failure on your
machine — that needs the native build deps. But the next `just
dev` will tell you exactly which one's missing, and once that's
installed the sidecar bundle will work.

Follow-up: once MyOwnMesh cuts a release with Phase A's IPC ops
landed (it currently has them on main but no release tag), this
code can switch to downloading the prebuilt
`myownmesh-<triple>.{tar.gz,zip}` release archive instead of
building from source. That'd skip the native-deps requirement
entirely. Tracking separately.

https://claude.ai/code/session_01Vp4cvRTaLYd3162EwwcCXg
You're right — 0.1.2 has Phase A's IPC ops, so we can skip the
whole webrtc-rs local-compile dance and just grab the prebuilt
daemon from GitHub Releases. Way more reliable on Windows where
the native-build chain (libsrtp / cmake / openssl) is brittle.

**`.myownmesh-rev`**: now holds the release tag (`v0.1.2`)
rather than a commit SHA. The build script branches on whether
the value starts with `v` to decide between the release-asset
path and the cargo-install fallback.

**`src-tauri/Cargo.toml`**: bumped the `myownmesh-core` git pin
to `tag = "v0.1.2"` so the library types the LLM compiles
against match exactly what the daemon binary ships. `Cargo.lock`
resolved to `add5a57b...`.

**`src-tauri/build.rs::bundle_myownmesh_sidecar`**: step 3 now
prefers `download_release_asset(tag, target_triple, ...)` which
shells out to `curl -fL` + `tar -xzf` (Linux/macOS) or
PowerShell's `Expand-Archive` (Windows) to grab the matching
release archive. Zero new build-time deps. The release matrix
in MyOwnMesh's `release.yml` ships one archive per target:
- `linux-x86_64` / `linux-aarch64` → `.tar.gz`
- `macos-aarch64` / `macos-x86_64` → `.tar.gz`
- `windows-x86_64` → `.zip`

target-triple → platform-name mapping mirrors that matrix.

If the download fails (offline build, GH rate-limited, asset
shape changed), falls back to `cargo install --git --tag v0.1.2`
with the captured-stderr diagnostic from the prior commit. This
path requires the native deps but at least surfaces *which* one
is missing instead of swallowing the error.

For the user's specific Windows machine: `just dev` will now
download `myownmesh-windows-x86_64.zip` (~10 MB), unzip
`myownmesh.exe` into `OUT_DIR/myownmesh-staging/`, copy it to
`binaries/myownmesh-x86_64-pc-windows-msvc.exe`, and Tauri's
bundler ships it next to `MyOwnLLM.exe` in the produced bundle.
First-launch then spawns the bundled daemon — no separate
install required.

https://claude.ai/code/session_01Vp4cvRTaLYd3162EwwcCXg
Three things visible in your last `just dev` log that this
fixes:

**1. `os error 32` during sidecar copy** — your prior `just dev`
was still running and Tauri had the sidecar open. The build's
final `fs::copy` couldn't overwrite a held file. Fixes:

- `binaries/.bundled-rev` sentinel records what's currently at
  the sidecar slot. At the top of `bundle_myownmesh_sidecar`, if
  the sentinel matches the requested rev AND the file is
  non-empty, the whole pipeline (download + extract + copy)
  skips. Next build with the parallel LLM still running just
  no-ops instead of fighting Windows.

- `write_sidecar_with_retry` exponential-backs-off (150ms →
  4.8s, 6 attempts) on `os error 32` specifically. Real "in use"
  collisions usually clear in a few hundred ms when the other
  process finishes opening the file. After the budget exhausts
  it surfaces the error so the user sees something is holding
  the file open rather than busy-looping forever.

**2. `find_daemon_binary` only tried `target\debug\myownmesh.exe`
and missed the actual sidecar** — Tauri's dev mode stages
sidecars with the `-<triple>` suffix retained
(`myownmesh-x86_64-pc-windows-msvc.exe`), while production
bundles strip it (`myownmesh.exe`). My old lookup only checked
the stripped name, missing the dev-mode location entirely. Now:

- `build.rs` emits `cargo:rustc-env=DAEMON_SIDECAR_TRIPLE=<triple>`
  so the runtime has the exact suffix Tauri uses.
- `daemon_binary_candidates` checks both
  `<exe_dir>/myownmesh{.exe}` (prod) AND
  `<exe_dir>/myownmesh-<triple>{.exe}` (dev) at the same
  priority level.
- New 1b: `<crate_dir>/binaries/myownmesh-<triple>{.exe}` — the
  *source* sidecar slot `build.rs` writes to. In `cargo run` /
  `tauri dev` paths that don't restage, this is the only place
  the bundled binary lives.
- All candidates filtered through a `push_if_usable` helper that
  skips zero-byte stubs (the `MYOWNLLM_SKIP_SIDECAR` placeholder
  files) — your log had this happen with a stale stub at
  `target\debug\myownmesh.exe`.

**3. `warning: function find_daemon_binary is never used`** — leftover
from renaming to `daemon_binary_candidates`. Marked
`#[allow(dead_code)]` since the CLI's diag still needs a
single-binary lookup for its `myownllm mesh` output.

After this lands, your next `just dev`:
- First run: downloads `myownmesh-windows-x86_64.zip`, extracts,
  copies to `binaries/myownmesh-x86_64-pc-windows-msvc.exe`,
  writes sentinel.
- Second run (while first is still running): sentinel matches,
  build.rs no-ops, no file-lock conflict.
- Runtime: finds the sidecar at
  `src-tauri/binaries/myownmesh-x86_64-pc-windows-msvc.exe`,
  spawns it successfully.

https://claude.ai/code/session_01Vp4cvRTaLYd3162EwwcCXg
You called it: shutdown was leaking processes. Two bugs in one
ungraceful-exit chain that your `Ctrl-C in the terminal` hit
every time:

**1. The LLM was getting orphaned**

`tauri dev` runs `cargo run` which spawns `myownllm.exe`. Tauri
windowed apps detach from the parent console, so a Ctrl-C in
your shell reached cargo but NOT myownllm.exe. The LLM kept
running after your terminal exited — that's what the
`watcher: another myownllm process holds the watcher lock` log
line was telling us. RunEvent::Exit never fired, Drop never ran,
and every file handle the LLM held stayed open (including the
sidecar slot you watched the build fail on).

Fix: a Windows parent-PID watchdog (`windows::install_parent_
watchdog`). At startup it finds our parent PID via
`CreateToolhelp32Snapshot` + Process32W, opens the parent for
`PROCESS_QUERY_LIMITED_INFORMATION`, then polls every second.
When the parent process exits, the watchdog calls
`std::process::exit(0)` — which DOES fire Drop +
RunEvent::Exit, so the daemon child + ollama get cleaned up
and every file handle is released before the LLM actually
terminates.

Skips when the parent is `explorer.exe` (interactive launch
from File Explorer) or `services.exe` (Windows service install
path) so it doesn't false-trigger.

CLI mode (`myownllm status` etc.) also skips since cargo
already manages lifetime properly via the attached console.

**2. The daemon child had no backstop**

Even when Drop DID run, the daemon child's cleanup relied on
the LLM's normal exit path. Any path that skipped Drop
(crash, taskkill /F, OS kill) would orphan the daemon. The
daemon then holds its own .exe file open and the next build's
copy fails with "file in use".

Fix: on Windows, the daemon child is now assigned to a Job
Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. The job
handle is leaked intentionally and held for the LLM process's
lifetime. When the LLM exits — graceful or not — Windows
reclaims the handle, the Job closes, every member process is
terminated. OS-level guarantee, not Rust-Drop-level.

Implementation: `windows::assign_to_kill_on_close_job(handle)`
wraps `CreateJobObjectW` + `SetInformationJobObject` +
`AssignProcessToJobObject`. Plumbed via
`DaemonChild { child, job }` in `ensure_daemon_running`.

**For your current orphan state**: open Task Manager (or
`taskkill /F /IM myownllm.exe`) once to clear the stuck process
holding the file. After that, `just dev` should be clean: the
new LLM exits properly when Ctrl-C kills cargo, the daemon dies
with it, no file locks survive.

https://claude.ai/code/session_01Vp4cvRTaLYd3162EwwcCXg
Your diagnosis was right. The download succeeded but something
downstream was corrupting it, producing a non-PE file at
`target\debug\myownmesh.exe` that spawn rejected with the
unhelpful "%1 is not a valid Win32 application" error. The
build pipeline was silent about which step failed, so we
couldn't tell whether curl, Expand-Archive, or the final copy
was the culprit.

**Step-by-step diagnostics in build.rs**:

Every stage now emits a `cargo:warning` so the log narrates
what actually happened:

  [download] https://github.com/.../myownmesh-windows-x86_64.zip
  [download] saved C:\...\OUT_DIR\myownmesh-staging\myownmesh-windows-x86_64.zip (10485760 bytes)
  [extract] C:\...\myownmesh-windows-x86_64.zip
  [extract] produced C:\...\myownmesh.exe (245678912 bytes)
  [sidecar] wrote C:\...\binaries\myownmesh-x86_64-pc-windows-msvc.exe (245678912 bytes)

PowerShell + tar invocations now capture stderr too, so a
malformed zip surfaces the actual error instead of just "exited
with 1".

**Post-extract validation**:

`validate_executable_magic` checks the first 4 bytes for PE
(`MZ`), ELF (`\x7fELF`), or Mach-O (`FEED FACE`/`FACF`/`CAFE
BABE`/byte-swapped). A corrupt download (HTML error page, empty
body, truncated transfer) gets caught at the build step instead
of producing a sidecar that spawn rejects at runtime.

The downloaded archive also gets a size sanity-check: <1 KiB is
almost certainly a GitHub error page rather than the daemon.

**Atomic write + corrupt-file self-heal**:

`write_sidecar_with_retry` now:
- Detects an existing sidecar slot file that fails the magic
  check (leftover stub, half-written previous attempt) and
  deletes it before writing — so Tauri's externalBin staging
  picks up the new content instead of re-staging the
  corruption.
- Writes via `<dst>.tmp-incoming` + atomic rename, so a failure
  mid-copy never leaves a partial file at the destination.
  This is what would have produced your truncated
  `target\debug\myownmesh.exe`: a copy that died partway
  through left a non-PE-but-non-empty stub at the dest, Tauri
  staged it, runtime spawn failed cryptically.

**Runtime magic-bytes filter**:

`mesh/daemon.rs::looks_like_executable` runs the same check
before treating a binary as a viable candidate. So even if a
corrupt file somehow ends up at any of the candidate paths, the
runtime skips it and tries the next instead of producing the
"not a valid Win32 application" error.

For your stuck state: delete
`src-tauri\binaries\myownmesh-x86_64-pc-windows-msvc.exe` and
`src-tauri\target\debug\myownmesh.exe` (whichever exist) before
the next `just dev`. The fresh build will detect any leftover
corruption and refuse to bundle it; the runtime will skip any
file that fails the magic check.

https://claude.ai/code/session_01Vp4cvRTaLYd3162EwwcCXg
Your last run pinpointed it: download (4 MB) ✓, extract (7.6 MB
binary) ✓, magic check ✓, but write to `binaries/` failed with
os error 32 (file in use) — the orphan LLM is still locking
the slot. Then at runtime the corrupt-but-MZ-starting file at
`target\debug\myownmesh.exe` (a partial PE from some earlier
failed copy) slipped through my too-lax 4-byte magic check and
spawn rejected it with "not a valid Win32 application".

Fixes:

**Stronger executable validation** — both `build.rs::
validate_executable_magic` and `mesh/daemon.rs::
validate_path_is_executable` now:

- Require file size ≥ 1 MiB (real daemon is ~7 MB; a 4 MB
  truncated PE would still fail this, but realistically a
  partial download is usually much smaller). A leftover stub
  or stub-sized garbage file gets rejected here.
- On Windows: walk the DOS header to `e_lfanew` (offset 0x3C),
  seek there, verify the `PE\0\0` signature. A truncated PE
  that starts with `MZ` but has no real PE header — exactly
  what your last run's `target\debug\myownmesh.exe` had — fails
  this check.

**Runtime self-heal** — when `daemon_binary_candidates` finds
an invalid file at a path the LLM owns (`src-tauri/binaries/`,
`src-tauri/target/<profile>/`), it now deletes the file
instead of just skipping it. The next build / Tauri dev cycle
rewrites cleanly instead of perpetually staging the same
corrupt bits. Files at paths we don't own (env-var overrides,
PATH lookups, sibling MyOwnMesh target) are skipped but not
touched.

So your next `just dev`:

1. Build downloads + extracts cleanly (as it did last run).
2. Write to `binaries/` may still fail if the orphan LLM is
   still locking it — but on the NEXT run, the runtime detects
   the stale corrupt file at `target\debug\myownmesh.exe`,
   deletes it, falls through to `binaries/myownmesh-<triple>.exe`
   which now has the freshly extracted 7.6 MB binary, spawns
   that.
3. Once the orphan is gone (parent watchdog from the prior
   commit handles that on first clean exit), everything
   stabilises.

For the orphan that's currently holding the file: a one-time
`taskkill /F /IM myownllm.exe` (or just close the LLM window
+ wait for the watchdog to act on it) is still needed to break
the lock initially. After that the new defences keep things
clean.

https://claude.ai/code/session_01Vp4cvRTaLYd3162EwwcCXg
You were right that an orphan process wasn't the issue. Going
back to your actual log:

  [extract] produced ...\myownmesh.exe (7663104 bytes)
  myownmesh sidecar bundle failed: ...os error 32

Neither `[sidecar] existing` nor `[sidecar] wrote` printed, so
the failure was BEFORE write_sidecar_with_retry — at the
intermediate `fs::copy(&bin, &installed_bin)?` step in
bundle_myownmesh_sidecar. That copy reads from the freshly-
extracted `staging/myownmesh.exe`, which Windows Defender's
real-time protection opens to scan as soon as PowerShell
creates a new .exe. The fs::copy competes with the scanner and
gets os error 32. No process is "running" in the user-visible
sense — Defender's scan is invisible to taskkill / Task
Manager.

Two fixes:

**Eliminate the redundant intermediate copy.** The previous
flow was:
  staging/myownmesh.exe → OUT_DIR/myownmesh-staging/myownmesh.exe → binaries/myownmesh-<triple>.exe
The OUT_DIR step served no purpose (the sentinel is keyed
against the sidecar slot, not OUT_DIR). Now we go directly:
  staging/myownmesh.exe → binaries/myownmesh-<triple>.exe
via write_sidecar_with_retry. One copy instead of two; one
failure point instead of two.

**Retry on source-locked too, with longer budget.** Previously
write_sidecar_with_retry only retried on dst-locked errors;
the copy step retried on the COPY itself getting blocked
(which I had wrong: the fs::copy success/fail block branched
on the COPY's error not the rename's). Now both branches retry
with 10 attempts, 200ms → ~102s exponential backoff. Defender
on an unsigned 7+ MB exe can take 10s+; the increased budget
covers it. Each retry logs which side is locked
(`copy from <src>` vs `rename to <dst>`) so the diagnostic
points at the actual cause.

Now your next `just dev`:
- Download → extract → DIRECT sidecar write
- If Defender holds the freshly-extracted file: retry with
  backoff, [sidecar] copy from ... hit sharing violation
  attempt N/10 logged each round, until Defender releases
- After success: [sidecar] wrote ..., sentinel persisted
- Subsequent runs short-circuit on the sentinel

https://claude.ai/code/session_01Vp4cvRTaLYd3162EwwcCXg
@mrjeeves mrjeeves merged commit bb6d7db into main May 28, 2026
4 checks passed
@mrjeeves mrjeeves deleted the claude/llm-on-myownmesh-engine branch May 28, 2026 02:41
mrjeeves pushed a commit that referenced this pull request May 28, 2026
PR #204's sidecar bundling prefers a sibling MyOwnMesh checkout's
`target/<profile>/myownmesh` binary over the GitHub release
download, on the assumption that "if the user has a sibling
checkout, they want it." That assumption skipped a check the user
hit in the wild: the sibling target/ is whatever the user last
built, NOT necessarily the rev pinned in `.myownmesh-rev`.

Concrete failure: one device's sibling at v0.1.1 + pin at v0.1.2
→ build.rs copied the v0.1.1 binary; the daemon's startup log
reported `version="0.1.1"`. The user's other device had no
sibling target build → fell through to release download, got
v0.1.2. The two daemons couldn't peer because the wire-protocol
additions in v0.1.2's PR #16 (the RPC + typed-channel + capability
ops) aren't understood by v0.1.1.

Fix: when the sibling exists, run `<binary> --version` and
compare against the pin. On match, use the sibling. On mismatch
or unreadable version, loud warning + fall through to the
release download. The escape hatch for users hacking against a
non-pinned MyOwnMesh version (env var `MYOWNLLM_MESH_BIN` →
explicit binary path, handled in step 1) is unchanged and
bypasses the version check entirely.

Also write `.bundled-rev` when the sibling path succeeds so the
next build's idempotency short-circuit can find it.

Standalone `rustc --edition=2021 src-tauri/build.rs ...` clean
(the only diagnostic is the expected unresolved `tauri_build`
crate from the build-dep that lives outside this sandbox).
mrjeeves added a commit that referenced this pull request May 28, 2026
…#205)

* mesh: wire frontend network catalog into daemon, call start() at boot

PRs #203 and #204 landed the daemon plumbing and the sidecar bundle,
but left two gaps that left every install stuck pre-join after the
migration off Trystero:

1. `meshClient.start()` was never invoked. App.svelte called
   `meshClient.reconcile()` at boot — which now (in the daemon
   client) just refreshes peers without subscribing to
   `mesh://event` or advancing past phase=off. The status pill
   stayed at "Joining <handle>…" forever.
2. The frontend's saved-network catalog was never pushed into the
   daemon. The daemon started with `networks=0` regardless of what
   the user had configured in `~/.myownllm/config.json`, so even
   when start() ran it dead-ended at `joined_networks[0] ?? ""`.

Fix:

- App.svelte boot: call `meshClient.start()` instead of
  `meshClient.reconcile()`. Start owns event subscription, peer
  snapshot, RPC handler install, capability publish — all the
  things reconcile() doesn't do.
- mesh-daemon.svelte.ts `start()`: bootstrap the daemon's
  joined-network set from the frontend's active network. Single-
  active-network UX, so drop any daemon networks that aren't the
  current active. Idempotent — second launch sees the network
  already joined (daemon persisted it under MYOWNMESH_HOME) and
  is a no-op.
- mesh-daemon.svelte.ts `reconcile()`: when active network
  changes under us (Switch button, addNetwork-with-activate),
  stop + start so handler claims rebind under the new network
  and the daemon-side leave/join converges.
- config.ts: helpers (`networkConfigToDaemonShape`,
  `daemonAddNetwork`, `daemonRemoveNetwork`,
  `syncActiveNetworkToDaemon`) translate between the frontend's
  flat schema (`signaling_servers: string[]`, etc.) and the
  daemon's structured `myownmesh_core::config::NetworkConfig`
  shape (`SignalingConfig`, `StunServer { urls }`, etc.).
- start() also gains a short retry on `mesh_daemon_status` so a
  boot that races the daemon-spawn task in Rust's setup() doesn't
  surface as a hard error during the brief window before the
  state is `app.manage()`d.
- start() serialised via an `inflightStart` promise so a boot
  call + an early settings click can't double-bootstrap and leak
  the first event listener.

Mid-session settings edits to the active network's STUN / TURN /
signaling lists aren't auto-propagated — the daemon has no
network-update RPC, only add/remove. Documented in `reconcile()`'s
doc; toggle the network off+on to apply changes.

`pnpm run check` clean (164 files, 0 errors).
`pnpm run build` clean.

* build: gate sibling-workspace daemon binary on .myownmesh-rev match

PR #204's sidecar bundling prefers a sibling MyOwnMesh checkout's
`target/<profile>/myownmesh` binary over the GitHub release
download, on the assumption that "if the user has a sibling
checkout, they want it." That assumption skipped a check the user
hit in the wild: the sibling target/ is whatever the user last
built, NOT necessarily the rev pinned in `.myownmesh-rev`.

Concrete failure: one device's sibling at v0.1.1 + pin at v0.1.2
→ build.rs copied the v0.1.1 binary; the daemon's startup log
reported `version="0.1.1"`. The user's other device had no
sibling target build → fell through to release download, got
v0.1.2. The two daemons couldn't peer because the wire-protocol
additions in v0.1.2's PR #16 (the RPC + typed-channel + capability
ops) aren't understood by v0.1.1.

Fix: when the sibling exists, run `<binary> --version` and
compare against the pin. On match, use the sibling. On mismatch
or unreadable version, loud warning + fall through to the
release download. The escape hatch for users hacking against a
non-pinned MyOwnMesh version (env var `MYOWNLLM_MESH_BIN` →
explicit binary path, handled in step 1) is unchanged and
bypasses the version check entirely.

Also write `.bundled-rev` when the sibling path succeeds so the
next build's idempotency short-circuit can find it.

Standalone `rustc --edition=2021 src-tauri/build.rs ...` clean
(the only diagnostic is the expected unresolved `tauri_build`
crate from the build-dep that lives outside this sandbox).

* fmt: rustfmt collapse of sibling-version error string

---------

Co-authored-by: Claude <noreply@anthropic.com>
mrjeeves added a commit that referenced this pull request May 28, 2026
…on (#207)

The migration off Trystero onto the standalone myownmesh daemon
(PRs #201 / #203 / #204 / #205 / #206) shipped the code but left
every doc still describing the world before the move:

- README claimed mesh discovery went "via Trystero over public
  Nostr relays" and that agent permissions persisted under
  `Config.agent_permissions.by_device[<device_id>]`.
- ARCHITECTURE.md's mesh-module section described `mesh-client.svelte.ts`
  (deleted), Trystero room ownership (gone), and a TS module table
  that didn't list any of the files Phase C–D actually shipped
  (`mesh-daemon.svelte.ts`, `mesh-gossip.ts`, `mesh-inference.ts`,
  `mesh-file.ts`, `mesh-move.ts`, `mesh-transcribe.ts`,
  `mesh-governance.ts`).
- CONNECTION-ENGINE.md was a 535-line spec for the 4-layer
  connection engine that no longer lives in this repo — every
  paragraph referenced `src/mesh-client.svelte.ts` or
  `mesh-scheduler-worker.ts`, neither of which exists.
- DOCS.md's Cloud Mesh section walked the user through Trystero
  rooms, the legacy on-the-wire `MeshMessage` JSON envelope
  (`infer_request` / `infer_chunk` / `move_offer` / `file_offer`),
  and a config example missing every field the per-network
  schema gained (`label`, `kind`, `topology`, `auto_approve`,
  `auto_gossip`, `agent_permissions`, `prompts`).
- PROGRESS.md was a historical bug-fix doc for a Trystero
  subscription-state quirk that no longer applies — the engine
  isn't here anymore.

What this commit changes:

**README.md**: replace Trystero claim with the bundled
`myownmesh` daemon model; correct the agent-permissions storage
path to the per-network shape (`Config.cloud_mesh.networks[*].
agent_permissions`) and mention the `auto_gossip` gate.

**ARCHITECTURE.md**: rewrite the one-picture diagram to show
the daemon sidecar alongside Ollama; rewrite the mesh intro
paragraph; rewrite the `mesh/` Rust module row to describe
`daemon.rs`, `daemon_commands.rs`, the detect-and-share socket
order, and the relationship to `myownmesh_core`; rewrite the
TS module table to list every `mesh-*.ts` file actually in the
tree with its current role; refresh the CloudMesh sub-tab
inventory (Status / Settings / Connections / Graph / Governance
/ Activity / HTTP); refresh the persistence section to show
`daemon.sock` + the per-network config layout.

**CONNECTION-ENGINE.md**: rewrite as a short pointer. The
4-layer engine + 7-tier reconnect ladder live in MyOwnMesh now;
this doc explains what the LLM still owns on top (the layer-4
LLM-specific protocol), how the LLM talks to the daemon
(detect-and-share IPC), and lists the LLM-side RPC methods +
typed channels currently in use (`infer`, `transcribe`,
`file_offer` / `file_send` + `file_chunks/<id>`, `session_*` /
`move_*`, `catalog/announce`, `permissions/snapshot`,
`prompts/snapshot`).

**DOCS.md Cloud Mesh section**: replace the Trystero transport
paragraph with the daemon's detect-and-share model; refresh
every What-the-mesh-does-for-you row to match current behavior
(click-to-open, click-through Pull, file transfer wire shape,
permissions+prompts gossip with the auto_gossip gate, Graph
view, Governance view, no Phase-1/Phase-2 split); replace the
JSON-over-data-channel wire-protocol box with the daemon
RPC + typed-channel surface; refresh the example config to
include `label`, `kind`, `topology`, `auto_approve`,
`auto_gossip`, `agent_permissions`, `prompts`.

**PROGRESS.md**: deleted. The Trystero subscription-state bug
it documents doesn't apply post-daemon. Two `// see PROGRESS.md`
breadcrumbs in `src-tauri/src/asr/mod.rs` and
`src-tauri/src/diarize/cluster.rs` updated to free-standing
explanations.

Validation:
- `pnpm run check`: 164 files, 0 errors, 0 warnings.
- `grep -rn "Trystero\|trystero\|mesh-client\.svelte" --include="*.md" .`
  returns nothing.
- `grep -rn "PROGRESS.md" .` returns nothing.

https://claude.ai/code/session_01RLu1LdTgtxEDdzhybzqFrk

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants