Skip to content

release: v1.0.0#23

Merged
esifea merged 20 commits into
mainfrom
release/v1.0.0
Jul 20, 2026
Merged

release: v1.0.0#23
esifea merged 20 commits into
mainfrom
release/v1.0.0

Conversation

@jh-lee-cryptolab

Copy link
Copy Markdown
Contributor

Summary

Merge release/v1.0.0 into main.

  • IVF centroid routing: receive IVF centroids, verify content hash at receipt/cache load, return cluster assignments; atomic SetCentroids
  • Backend health: stop mistaking a saturated llama-server for a dead one; bound the embed HTTP call so a hung server can't wedge recovery; machine-readable embed precondition reasons
  • INST-7: bind the embedding socket at a short path when RUNED_HOME is deep; isolate long-path socket aliases per user
  • OPS-90: relicense runed MIT → Apache-2.0, ship LICENSE/NOTICE in installs and release archives
  • Rename vault → console
  • README refresh + release badge CI workflow

Test

  • go test ./... (new coverage: route/verify, server/centroids, backend verdict/timeout, ipc/resolve, bootstrap/licenses)

couragehong and others added 20 commits July 15, 2026 17:36
runed never dials the index engine, but inserts need a plaintext IVF
cluster assignment computed next to the embedding. The centroid set is
relayed runespace -> vault -> rune-mcp -> here:

- SetCentroids (client-stream): header + id-ordered batches, validated
  against the daemon's embedding dim, installed atomically, persisted to
  $RUNED_HOME/cache (gob, atomic rename) so restarts skip the re-push.
- Embed/EmbedBatch with_route: responses carry cluster_id and the
  centroid_set_version the assignment was routed against; requests fail
  fast with FAILED_PRECONDITION until a set is loaded.
- Info exposes centroid_set_version so callers can detect staleness.
- internal/route: max-inner-product assignment (lowest-id ties),
  mirroring runespace-go-sdk's insert routing exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Embed/EmbedBatch return FAILED_PRECONDITION for two opposite conditions:
the model is still loading (wait and retry the same call) vs no centroid
set is loaded (push one via SetCentroids, then retry — §9.2 C4). Clients
could only tell them apart by parsing the human message, and rune-mcp
misdiagnosed the bootstrap window as the centroid case: it fired a
pointless resync and surfaced a non-retryable error for a condition that
heals itself in ~30s (first capture after a machine reboot hits this).

Attach an ErrorInfo{reason, domain} detail — BOOTSTRAPPING or
NO_CENTROID_SET — the same pattern runespace uses for its error reasons.
The status code and message are unchanged, so the transport-level
non-retry decision (see TestServer_EmbedFailsBeforeBackendSet) and old
clients are unaffected; new clients branch on the reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the engine's exact version recipe (sha256 over dim LE, nlist LE,
preset+NUL, float32 bits LE in id order — runespace/internal/cluster/
centroid.go computeVersion) as route.ComputeVersion, and add
VerifyVersion to recompute it against the relayed Version tag.

Verification runs at exactly two entry points, never per-Embed:
  C2  SetCentroids receipt -> InvalidArgument on mismatch
  C5  gob cache Load       -> 'route: cache verify' error on bitrot

Requires the new 'string preset = 4' header field (the missing hash
ingredient). Empty preset = legacy sender -> verification skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- verify_test: TestComputeVersionRecipe now hashes the hand-assembled
  bytes and compares to ComputeVersion, so recipe drift (dim/nlist order,
  endianness, preset NUL) actually fails the test instead of the golden
  buffer being dead
- centroids_test: TestEmbedWithRouteRequiresCentroids wires a backend so
  the bootstrap gate passes, then asserts the NO_CENTROID_SET reason —
  previously it passed via the bootstrap error and never exercised the
  routing branch (would pass even with with_route=false)
- centroids_test: add a content-hash-mismatch case (preset present, tag
  wrong) to cover the SetCentroids VerifyVersion gate the no-preset cases
  skip
- add doc comments describing what each test verifies

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- add installMu guarding the in-memory Store + disk Persist so two
  concurrent pushes of different sets can't leave memory holding one
  set while the cache holds another (a restart would then silently
  serve the stale one); guards only the tail, not the Recv loop
- comment that header nlist is only a preallocation capacity hint —
  the authoritative count is len(cs.Vectors) and the content hash
  already pins the exact vectors, so header nlist isn't enforced

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under sustained embedding load (20 concurrent capture workers), EnsureStarted
entered a kill/restart loop: the single-slot llama-server starves its /health
endpoint while inference saturates it, the per-RPC 500ms probe missed, and one
missed probe was treated as death. Worse, the restart path called stopLocked
directly — skipping the inflightMu drain that Stop performs — so every queued
embed was severed with a connection EOF. The restarted child saturated again
immediately and the loop repeated every ~6s (observed: dozens of restarts over
3 minutes, 244 embeds cut, 240/1000 captures failed).

Three defenses, in escalation order:

1. Proof-of-life tracking: every completed embed (doJSON success) and every
   successful health probe stamps lastAlive. A running child with a proof of
   life within aliveGrace (15s) that misses the quick probe is busy, not dead
   — EnsureStarted leaves it alone. Under saturation embeds complete
   continuously, so this short-circuits almost every false alarm.

2. Drain-then-reprobe (restartIfDead): with no life signs, take the
   inflightMu writer lock — which by construction waits for every in-flight
   embed, so nothing is ever severed — then re-probe with a generous budget.
   A merely-saturated child empties its queue during the drain and answers;
   the restart is skipped. restartMu single-flights bursts of suspicious
   RPCs. Lock order restartMu → inflightMu → lifecycleMu (Stop keeps its
   inflightMu → lifecycleMu suffix).

3. Only a child that stays unresponsive with zero in-flight work is killed
   and relaunched.

Serving() gets the same busy-vs-dead awareness so health dashboards stop
reporting DEGRADED during every heavy burst.

Regression tests (verdict_test.go, -race): a busy child with fresh life signs
is left alone; a child that recovers after the drain is not restarted and the
in-flight embed completes un-severed; a truly dead child is replaced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… test

Review findings on the busy-vs-dead verdict:

1. restartIfDead did not re-check proof of life after acquiring restartMu.
   Followers queued behind a leader (whose drain let embeds complete and
   stamp lastAlive) would each run their own 2s reprobe and — under renewed
   saturation — their own full drain cycle in series. Re-check recentlyAlive
   first; the leader's drain or a restart's startup health poll has usually
   stamped it, so followers exit immediately.

2. TestEnsureStartedDrainRecoverySkipsRestart synchronized on a 100ms sleep
   to assume the in-flight embed held its RLock; on a slow runner the drain
   could acquire instantly while /health still reported busy, restarting a
   healthy child and flaking the test. Replace the sleep with the same
   received-channel handshake inflight_test.go uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…recovery

doJSON used http.DefaultClient (no timeout) with the caller's ctx, which
carries no deadline on the embed path (the server handler forwards the gRPC
ctx unchanged). A llama-server that accepts the TCP connection but never
responds — a hang, not a crash, since a crash resets the connection — would
block the call forever. That call holds inflightMu.RLock, so it would also
wedge restartIfDead's inflightMu.Lock() drain: the recovery path that exists
to replace a stuck server would itself hang on the stuck server.

Wrap each embed request in context.WithTimeout(ctx, embedRequestTimeout=120s)
(WithTimeout takes the earlier of the caller's deadline and the ceiling). The
ceiling is far above any legitimate embed, including the ~10s deep-queue waits
seen under heavy concurrency, so it only trips on a genuinely wedged backend;
the request then errors, releases the RLock, and the next EnsureStarted
restarts the child.

TestStuckServerDoesNotWedgeDrain: a fake server that never responds — the
embed returns on the timeout and a subsequent inflightMu.Lock drains, proving
the recovery path no longer hangs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (review)

Per review: probeHealthy is now a pure "does /health answer 200?" query with
no side effect, and IsHealthy records proof of life on success. The direct
probeHealthy callers that only decide whether to restart (EnsureStarted's
quick probe, restartIfDead's verdict probe) no longer reset the liveness clock
as a side effect of asking.

Measured trade-off (documented on IsHealthy, TestQuickProbeStampEdge): dropping
the quick-probe stamp reopens one narrow edge — idle >aliveGrace, a slow first
embed, then a concurrent second request whose quick probe is starved — where
that second request's EnsureStarted escalates into restartIfDead and pays the
verdict probe (~1.7s vs ~0.5s before). It never false-restarts, never blocks on
the drain, and self-corrects on the next embed completion, so at most one
request in a rare window sees the extra latency — acceptable for the cleaner
separation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No Python file has ever existed in this repo's history; the
scripts/.venv/ and __pycache__/ entries came along with the initial
public import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…E is deep

A UNIX domain socket path must fit sockaddr_un.sun_path — 104 bytes on
macOS, 108 on Linux. When $RUNED_HOME sits under a deep directory (long
scratchpad HOMEs, sandboxes), $RUNED_HOME/embedding.sock exceeds that and
BOTH sides of the IPC failed: the daemon's bind died with the cryptic
"bind: invalid argument", and the client's connect() hit the same limit,
so fixing either side alone was useless.

Introduce ipc.ResolveSocketPath: paths under 104 bytes pass through
unchanged; a path at/over the limit maps to /tmp/runed-<hash>.sock, where
<hash> is the first 8 bytes of sha256(cleaned canonical path) in hex. The
alias is deterministic — daemon and client each resolve the same canonical
path and converge on the identical short alias with no handshake — and
collision-safe across users/homes because the hash covers the full path.
/tmp is joined literally (not os.TempDir(), which on macOS is itself a
long /var/folders/... path). The resolver is idempotent, so defensive
re-resolution at any bind/dial site is harmless.

Applied at every bind/dial surface:
- ipc.Listen resolves before binding (stale-socket cleanup, 0700 chmod,
  and ownership tracking all operate on the alias).
- cmd/runed resolves sockPath up front so the reachability probe, stale
  removal, and self-eviction watchdog match what Listen binds.
- client dials the resolved alias but keeps the canonical path for
  spawn.EnsureDaemon, which derives the daemon's RUNED_HOME from it.
- spawn.probeDaemon resolves before stat/dial; the spawn.lock and log
  paths stay under the canonical home.

Windows named pipes are unaffected; the resolver is pure and untagged, so
GOOS=windows compiles as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
runed redistributes llama-server (llama.cpp, MIT) and the Qwen3-Embedding
GGUF weights (Apache-2.0) via self-bootstrap, but carried no license text at
all — not even its own (README said "LICENSE file forthcoming"). MIT requires
the copyright + permission notice to accompany all copies, and Apache-2.0 §4
requires a copy of the license with redistribution, so this was a live
compliance gap on every machine that ran the bootstrap.

- LICENSE: runed's own MIT text (canonical wording; copyright line
  "Copyright (c) 2026 CryptoLab, Inc." — entity/year subject to legal review).
- THIRD_PARTY_LICENSES/: byte-exact upstream texts, fetched from source —
  llama.cpp LICENSE (ggml authors, MIT) and the canonical Apache License 2.0
  (apache.org, sha256 cfc7749b…) for Qwen3-Embedding-0.6B, plus a factual
  attribution index. The Qwen HF repos declare license:apache-2.0 and ship no
  LICENSE/NOTICE of their own, so no NOTICE contents exist to reproduce
  (§4(d) applies only when the work includes one) — none were invented.
- licenses.go embeds the texts into the daemon binary; bootstrap now installs
  them to $RUNED_HOME/licenses/ on every EnsureAll (idempotent, non-fatal on
  write failure), so the artifacts and their licenses always travel together.
- README's License section now points at the real files.

Out of scope (release-pipeline work): baking the texts into the re-hosted
llama-server tarball itself, and legal sign-off on the attribution wording.

Verified: go build/vet clean, all 9 packages pass, new tests prove the
installed copies are byte-identical to the embedded texts and self-heal on
rerun; upstream texts diff-verified against their sources at fetch time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rywhere the LICENSE goes

- LICENSE: byte-exact apache.org LICENSE-2.0.txt
  (sha256 cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30)
- NOTICE: Apache-2.0 §4(d) attribution (runed / Copyright 2026 CryptoLab, Inc.)
- NOTICE rides with the LICENSE at every redistribution point: go:embed
  LicenseFS → $RUNED_HOME/licenses/ install, release tarball contents,
  and the release workflow's tarball verification list
- README + THIRD_PARTY_LICENSES/README attribution index updated (third-party
  texts unchanged: llama.cpp stays MIT, Qwen3 GGUF stays Apache-2.0)

Verified: go build ./... + full test suite (installLicenses now asserts the
NOTICE lands verbatim), LICENSE checksum matches the canonical text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@esifea
esifea merged commit e4981a5 into main Jul 20, 2026
6 checks passed
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.

3 participants