Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
158 commits
Select commit Hold shift + click to select a range
c837f16
test(distributed): share suite containers, isolate specs by database
mudler Aug 31, 2026
d013619
test(distributed): stop leaking admin pools when database setup fails
mudler Aug 31, 2026
4c8b8c3
test(distributed): wait for the log subscriber instead of racing it
mudler Aug 31, 2026
1dc7998
test(distributed): scope the log-subscriber wait and mark the race it…
mudler Aug 31, 2026
05bc519
ci(distributed): run the distributed e2e suite on PRs
mudler Aug 31, 2026
f5e90f2
ci(distributed): widen the trigger and drop the mid-suite image pull
mudler Aug 31, 2026
8533a10
test(distributed): add a process-level cluster harness
mudler Aug 31, 2026
0cf3981
test(distributed): make the cluster harness survive a restart
mudler Aug 31, 2026
84ac07d
test(distributed): add admin session helper to the cluster harness
mudler Aug 31, 2026
1d74675
test(distributed): give each frontend its own data dir and one pinned…
mudler Aug 31, 2026
941150a
test(distributed): add kill and restart primitives to the cluster har…
mudler Aug 31, 2026
6e78ea9
test(distributed): correct the failure-primitive comments and guard t…
mudler Aug 31, 2026
3d2a96d
test(distributed): prove the cluster harness with a two-replica baseline
mudler Aug 31, 2026
9862d9e
test(distributed): fail rather than skip the cluster specs in CI
mudler Aug 31, 2026
9b0d382
test(distributed): require the cluster binaries by default under CI
mudler Aug 31, 2026
8990c51
test(distributed): cover replica death, rolling restart and racing re…
mudler Aug 31, 2026
4e079c9
test(distributed): prove health checking was alive during the failove…
mudler Aug 31, 2026
c3458e9
ci(distributed): run the process-level cluster suite
mudler Aug 31, 2026
8e8a5d3
ci(distributed): fail the cluster job when it selects no specs
mudler Aug 31, 2026
1dae0ce
docs: document the distributed and cluster e2e suites
mudler Aug 31, 2026
235057b
docs: correct the flake-attempts semantics and fill the review gaps
mudler Aug 31, 2026
1e5f5db
test(distributed): correct the claims the e2e comments make
mudler Aug 31, 2026
15e325c
docs(e2e): correct three claims in the distributed e2e comments
mudler Aug 31, 2026
c72baa4
feat(cluster): record frontend replicas in a shared instances table
mudler Aug 31, 2026
42be69d
fix(cluster): stamp liveness on the database clock and refuse undiala…
mudler Aug 31, 2026
6caceb4
feat(cluster): add the bidirectional splice used by the relay and tunnel
mudler Aug 31, 2026
c05246a
fix(cluster): treat a yamux teardown in Splice as a normal ending
mudler Aug 31, 2026
a0deca6
fix(cluster): report a dead yamux session instead of swallowing it
mudler Aug 31, 2026
c2162f7
feat(cluster): accept authenticated peer links on /api/cluster/peer
mudler Aug 31, 2026
73da808
fix(cluster): stop reading a bare EOF as a clean ending
mudler Aug 31, 2026
9fdd0be
fix(cluster): own the peer auth prefix in auth, and pin what the spec…
mudler Aug 31, 2026
c7cb7e1
feat(cluster): dial and pool yamux links to peer replicas
mudler Sep 1, 2026
393797e
refactor(cluster): make the cluster service a leaf and blame the call…
mudler Sep 1, 2026
edc7313
feat(cluster): fence worker-connection ownership with a monotonic epoch
mudler Sep 1, 2026
6f53701
fix(cluster): draw connection epochs from a sequence so none is ever …
mudler Sep 1, 2026
e951433
fix(cluster): keep the connection schema migratable on SQLite
mudler Sep 1, 2026
dca8ce7
feat(cluster): give phase 1 a call site, and prove it against real re…
mudler Sep 1, 2026
bb4719e
fix(cluster): hold the guarantees phase 1's comments were claiming
mudler Sep 1, 2026
3daf889
fix(cluster): share one lock order, and correct the phase 1 comments
mudler Sep 1, 2026
a275147
feat(cluster): resolve tunnel ownership against a live owner
mudler Sep 1, 2026
df7cebf
fix(cluster): make the ownership comments say only what holds
mudler Sep 1, 2026
032725c
feat(cluster): hold worker tunnels, and re-claim them after a sweep
mudler Sep 1, 2026
2466e42
fix(cluster): make a claim and its record indivisible per node
mudler Sep 1, 2026
9e4df19
fix(cluster): keep a session close out of the per-node claim gate
mudler Sep 1, 2026
9702e01
feat(cluster): open the door a worker dials its tunnel through
mudler Sep 1, 2026
7deda4a
fix(cluster): harden the worker tunnel, and stop starting a database …
mudler Sep 1, 2026
0762fc0
fix(testutil): stop the shared-database change from disarming two reg…
mudler Sep 1, 2026
7746bcf
fix(testutil): clear the maintenance timeouts at connection startup
mudler Sep 1, 2026
148fff1
feat(worker): dial, hold and serve the tunnel, on a credential of its…
mudler Sep 1, 2026
9bf2013
fix(worker): spec the tunnel's routing table, which was the SSRF boun…
mudler Sep 1, 2026
9095480
fix(worker): make the tunnel credential's node-type gate actually str…
mudler Sep 1, 2026
08aa37b
feat(cluster): relay a peer's stream onto a worker tunnel held here
mudler Sep 1, 2026
1f3dc70
fix(cluster): express the splice policy once, and pin the relay's bud…
mudler Sep 1, 2026
b1dc6c4
feat(cluster): reach every worker through its tunnel, never its address
mudler Sep 1, 2026
d26d8b9
fix(cluster): make "no route" a condition of its own, and let it out …
mudler Sep 1, 2026
b0a51f5
fix(grpc): let the transport answer through the wrappers, not only pa…
mudler Sep 1, 2026
050e585
test(nodes): make the joiner spec deterministic, and gate the wrapper…
mudler Sep 1, 2026
21f1fcc
docs(grpc): withdraw the lint-cost claim, and make the one nolint tid…
mudler Sep 1, 2026
256d4ee
feat(distributed): stop workers listening, and stop them advertising
mudler Sep 1, 2026
4ce7efd
fix(distributed): refuse a worker that cannot tunnel, and say why it …
mudler Sep 1, 2026
d508964
test(distributed): prove the worker tunnel end to end, under real inf…
mudler Sep 1, 2026
5f5baf9
fix(cluster): stop blaming a peer for the caller's own expired deadline
mudler Sep 1, 2026
92e1927
fix(distributed): let a worker's own refusal be evidence about its ba…
mudler Sep 2, 2026
00a2645
fix(cluster): stop a late request frame reading as the worker's verdict
mudler Sep 2, 2026
e209492
fix(cluster): keep the refusal vocabulary in one table
mudler Sep 2, 2026
db31cf5
feat(cluster): record a departure instead of erasing the connection
mudler Sep 2, 2026
f8bece3
fix(cluster): pin the departure retention to the loop that applies it
mudler Sep 2, 2026
c4e5587
feat(cluster): answer presence with four values on the database clock
mudler Sep 2, 2026
29a0fd3
fix(cluster): pin the derived retention to the sweep that applies it
mudler Sep 2, 2026
9b611f0
feat(worker): serve the control plane over the tunnel, not over NATS
mudler Sep 2, 2026
3b836bc
fix(worker): pin the rune cut, answer unload honestly, drop the dead …
mudler Sep 2, 2026
96e259f
feat(distributed): call the worker's control routes instead of the bus
mudler Sep 2, 2026
0494202
test(distributed): pin the no-demotion rule at every call site it is …
mudler Sep 2, 2026
dccc536
feat(distributed): serve file staging over the worker tunnel
mudler Sep 2, 2026
fd1eb34
test(distributed): pin the unreadable-request rule at all three file …
mudler Sep 2, 2026
1b12d87
feat(distributed): read worker absence from the database, not from a …
mudler Sep 2, 2026
c0b3246
fix(distributed): let absence be decided by something, at all three c…
mudler Sep 3, 2026
52c099e
feat(distributed): take the backend worker off NATS entirely
mudler Sep 3, 2026
1ed2222
test(distributed): prove phase 3 end to end, under real processes
mudler Sep 3, 2026
b35e1b4
refactor(distributed): one subject matcher, shared by the carrier and…
mudler Sep 3, 2026
e7d322c
feat(distributed): carry fan-out on PostgreSQL LISTEN/NOTIFY
mudler Sep 3, 2026
43956a9
fix(distributed): order LISTEN and UNLISTEN on one lock
mudler Sep 3, 2026
b8d514d
feat(distributed): keep the listener draining and let it come back
mudler Sep 3, 2026
7629804
feat(distributed): give agent workers a tunnel of their own
mudler Sep 3, 2026
7c7324b
feat(distributed): make MCP execution and discovery a selection
mudler Sep 3, 2026
4c2d696
test(distributed): name the replica endpoint the router now requires
mudler Sep 3, 2026
0d61cc1
feat(distributed): stop a backend on one route, whatever the worker is
mudler Sep 3, 2026
8c6c434
test(e2e): build the cluster binary and drop a load-sensitive budget
mudler Sep 3, 2026
ae3ff21
feat(distributed): let a worker name a broadcast the frontend decides on
mudler Sep 3, 2026
dc0c50f
feat(distributed): dispatch queued work as a claim queue
mudler Sep 3, 2026
236c3a0
feat(distributed): give responses.metadata something to re-hydrate from
mudler Sep 3, 2026
a3443c3
fix(distributed): give one tenant's agent tasks a subject of their own
mudler Sep 3, 2026
27e2ead
feat(distributed): carry the state.*.delta families on PostgreSQL
mudler Sep 3, 2026
0d2d4f0
refactor(distributed): move job and agent fan-out onto the PostgreSQL…
mudler Sep 4, 2026
000545b
feat(distributed): move the last nine fan-out families onto PostgreSQL
mudler Sep 4, 2026
eaf4eb6
refactor(distributed): delete MessagingClient and shrink the NATS cli…
mudler Sep 4, 2026
dc34e0c
feat(distributed): carry an agent cancel on the worker's own tunnel
mudler Sep 4, 2026
60221f0
fix(distributed): read a departed agent tunnel as the routing fact it is
mudler Sep 4, 2026
b91ed7a
chore(distributed): stop telling an operator to run a NATS cluster
mudler Sep 4, 2026
646fd69
chore(distributed): take the nats-io modules out of the build
mudler Sep 5, 2026
cf62a72
test(distributed): prove the busless cluster on two frontends and two…
mudler Sep 5, 2026
276e38f
test(distributed): prove the fan-out carrier between two real replicas
mudler Sep 6, 2026
fbfbb85
feat(cluster): make a peer prove which replica it is
mudler Sep 6, 2026
9f1939c
fix(distributed): say that skills and collections are replica-local
mudler Sep 6, 2026
c56fcec
fix(worker): check a gRPC port is free before handing it out
mudler Sep 6, 2026
a5f54b7
fix(distributed): bound the health monitor's miss streaks to live rows
mudler Sep 6, 2026
d42d70e
fix(distributed): drop a departed node's cached HTTP clients
mudler Sep 6, 2026
8f5f303
fix(distributed): back a failed claim off instead of respinning it
mudler Sep 6, 2026
55bc99c
test: fix the three data races that made -race runs noisy
mudler Sep 6, 2026
22ef89c
test(worker): script the kernel out of the port bookkeeping specs
mudler Sep 6, 2026
3aab79f
test(openai): drop the audio snapshot nothing reads
mudler Sep 6, 2026
3cce3e3
test: guard shared PostgreSQL setup on macOS
localai-org-maint-bot Sep 7, 2026
595dfb9
test: keep distributed specs in their own targets
localai-org-maint-bot Sep 7, 2026
553d11a
fix(worker): resolve staging directory symlinks
localai-org-maint-bot Sep 8, 2026
06aaed9
test(worker): canonicalize missing upload fixture
localai-org-maint-bot Sep 8, 2026
5612dd1
fix(distributed): preserve master protocol extensions
mudler Sep 14, 2026
bc8e843
test(distributed): use renamed worker address
localai-org-maint-bot Sep 15, 2026
4f6648b
test(nodes): match the frontend setup command
localai-org-maint-bot Sep 15, 2026
b06149b
docs(distributed): define binary conformance
mudler Sep 15, 2026
58d0bfa
docs(distributed): test machine authentication
mudler Sep 15, 2026
a626c9c
fix(distributed): offline stale unhealthy nodes
mudler Sep 15, 2026
c06dd46
fix(distributed): accept legacy empty stop replies
mudler Sep 15, 2026
6f1f0aa
test(distributed): migrate before restricting DB
mudler Sep 15, 2026
189439f
test(distributed): pin cluster health timings
mudler Sep 15, 2026
9bd9139
fix(ui): complete node operation journeys (#12070)
localai-org-maint-bot Sep 15, 2026
57736e5
test(distributed): add deterministic backend fixtures
mudler Sep 15, 2026
5b308fe
fix(mock-backend): harden deterministic fixtures
mudler Sep 15, 2026
42e525d
test(distributed): cover binary backend routes
mudler Sep 15, 2026
f746c3c
test(distributed): complete binary backend conformance
mudler Sep 15, 2026
1cebf6a
fix(distributed): harden binary conformance
mudler Sep 15, 2026
6dc6f8d
fix(distributed): stage directory outputs
mudler Sep 15, 2026
7fafe3c
fix(distributed): preserve async staging state
mudler Sep 15, 2026
c1f102e
fix(distributed): close staging conformance gaps
mudler Sep 15, 2026
ed5ab1b
fix(distributed): harden staged resource ownership
mudler Sep 15, 2026
5822ec1
test(distributed): guard backend protocol coverage
mudler Sep 15, 2026
8892940
test(distributed): scan staging overrides package-wide
mudler Sep 15, 2026
d14f93a
test(distributed): verify binary machine authentication
mudler Sep 15, 2026
9cf56f4
fix(distributed): prove agent worker credentials
mudler Sep 15, 2026
08f3b4d
test(distributed): integrate binary CI gates
mudler Sep 15, 2026
14fc9d5
test(distributed): align conformance specs with Ginkgo
mudler Sep 15, 2026
0a81ac4
test(distributed): reap binary backend children
mudler Sep 15, 2026
85d1e61
test(distributed): correct cluster suite docs
mudler Sep 15, 2026
2637573
fix(vram): bound remote GGUF metadata lengths
mudler Sep 15, 2026
3a87afb
fix(deps): remove stale GGUF parser sums
mudler Sep 15, 2026
9e944ab
fix(security): clear the gosec findings this branch introduced
mudler Sep 18, 2026
8c2e324
fix(security): justify the last two gosec findings in new code
mudler Sep 18, 2026
6cb1a2f
fix(distributed): follow current backend protocol
mudler Sep 19, 2026
90876e3
test(worker): canonicalize shared staging paths
localai-org-maint-bot Sep 20, 2026
4d336db
test(ui): open node setup before checking commands
localai-org-maint-bot Sep 26, 2026
7890e5f
test(ui): wait for the automatic worker drawer
localai-org-maint-bot Sep 27, 2026
60b1f38
chore: merge master into distributed transport PR
localai-org-maint-bot Sep 28, 2026
12be82c
chore: merge master into distributed transport PR
localai-org-maint-bot Sep 28, 2026
02a2988
chore: merge master into distributed transport PR
localai-org-maint-bot Sep 28, 2026
58e95a8
chore: merge master into distributed transport PR
localai-org-maint-bot Oct 1, 2026
7c1fdd2
fix(nodes): confine hash sidecar reads and writes
localai-org-maint-bot Oct 1, 2026
b4f6e3e
fix(tests): use recorded realtime events
localai-org-maint-bot Oct 1, 2026
799e739
test(nodes): use Ginkgo for hash sidecar tests
localai-org-maint-bot Oct 1, 2026
b941fc4
chore: merge master into distributed test branch
localai-org-maint-bot Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
23 changes: 23 additions & 0 deletions .agents/building-and-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,26 @@ Rules (both gates):
- **Don't weaken the gate:** never hand-lower a baseline or widen a tolerance to turn a red gate green. The ratchet only moves up.
- If a change drops coverage, **add tests** (sort `coverage-summary.json` by line% ascending to find untested code) rather than editing the baseline. When coverage legitimately rises, commit the regenerated baseline (`make test-coverage-baseline` / `test-ui-coverage-baseline`).
- The Go gate is **strict — no tolerance**; `covermode=atomic` keeps it deterministic. The UI gate keeps a small tolerance only because its e2e coverage isn't.

## Distributed-mode test suites

Two suites cover distributed mode (frontend replicas, worker nodes, PostgreSQL), split by a Ginkgo label. Neither starts a message broker: a distributed deployment needs PostgreSQL and the frontends' own HTTP listener, and the `nats-io` modules are not in the build.

- `make test-e2e-distributed` runs `Distributed && !VLLMMultinode && !Cluster` over `./tests/e2e/distributed` recursively. Services are wired directly into the test binary. The root package runs 224 feature specs plus 4 fast protocol-coverage inventory specs (228 total), and the subpackage adds 8 argument-validation specs; the combined target ran in 2m12s on the latest measured run. The inventory fails when a generated backend RPC or file-staging override is added without a coverage classification.
- `make test-e2e-cluster` runs `Cluster` and spawns real `local-ai` child processes through the `tests/e2e/distributed/cluster` helper package. It **builds `./local-ai` itself** (the `e2e-binary` target), so you do not run `make build` first and cannot forget to. Its 30 specs include the `BinaryConformance` full backend/staging matrix and the `MachineAuth` browser/machine trust-domain scenario. The latest run measured 948.5s of specs and 15m51s for the target with an incremental build.

Both jobs live in `.github/workflows/tests-e2e-distributed.yml`, with `timeout-minutes: 45` each. They trigger on pull requests *and* on every push to `master`; the `paths-ignore` filter (see [.agents/ci-caching.md](ci-caching.md)) sits on the pull-request trigger only, so a master push always runs both. They are advisory only because `master` carries no branch protection, which is a repository setting and not a YAML key: `continue-on-error: true` would flip the run's *conclusion* to success and hide the failure, so it is not used.

- **The container is suite-scoped, not spec-scoped.** `SetupInfra` used to start a PostgreSQL (~10s) and a broker (~3.5s) per spec. Across the 213 specs behind it that was roughly **48 minutes of pure container startup per run**, which is why this suite was never in CI. (213 rather than the ~240 above: the larger number is everything the label filter selects, the smaller one is just the specs that call `SetupInfra`.) The broker container is gone entirely, and the PostgreSQL one now starts once in `BeforeSuite` with each spec getting its own database via `CREATE DATABASE` (~67ms), which is what the `dbName` argument was always describing. Adding a spec needs no change: call `SetupInfra("some-name")` as before, the name is a prefix and a counter keeps it unique.
- **Consequence for new specs:** a fan-out carrier is *shared* within a Ginkgo process, so a wildcard subscriber can observe another spec's traffic. Filter assertions on an identifier your spec owns (a node ID, a job ID) instead of counting everything on `jobs.*.progress`, and verify the spec with `--randomize-all`.
- **`BeforeSuite`, not `SynchronizedBeforeSuite`.** Under `ginkgo -p` each process then gets its own container, keeping each process's carrier isolated. A single shared database across parallel processes would let specs on different processes see each other's messages on the same subject.
- **The label split.** The 8 argument-validation specs under `tests/e2e/distributed/cluster/` carry `Label("Distributed")` only, on purpose: they need no binary and no PostgreSQL, so they belong in the fast job. That is why `test-e2e-distributed` keeps `-r` (it must reach the subpackage) and `test-e2e-cluster` deliberately does **not** (the subpackage is out of its scope).
- **The binary is built, and a stale one is refused.** `test-e2e-cluster` used to take `./local-ai` as given and only check that a file by that name existed. An edit that was never rebuilt therefore ran the entire suite against an older build while reporting on the working tree, which surfaced once as a spec failing on caller line numbers that matched no line in any source file. Two things close it. The target depends on `e2e-binary`, which is `go build -o local-ai ./cmd/local-ai` and not `make build` (that one also builds the React UI, which this suite never touches and CI stubs). And `localAIBinary` in the harness **fails**, on a developer box as well as under CI, when the binary is older than the newest non-test Go source in the tree, which covers a run started with `LOCALAI_E2E_BINARY` or by invoking ginkgo directly. `_test.go` files are excluded from that scan on purpose: they compile into the ginkgo suite and never into `local-ai`, so counting them would fire on every edit to the spec being run. Set `LOCALAI_E2E_BINARY` to opt out of the build; there is no opt-out of the staleness check, because a rebuild is the remedy.
- **`--fail-on-empty` is load-bearing on both targets.** Ginkgo exits 0 when a label filter selects nothing, so without it a refactor that renames or drops `Label("Cluster")` leaves the target reporting "Test Suite Passed" having started no cluster at all. `LOCALAI_E2E_REQUIRE_BINARIES` does not cover this case: it only fires inside a spec that is actually running.
- **The binary gate.** `localAIBinary()` and `mockBackendBinary()` **fail** rather than skip when `CI` is set, or when `LOCALAI_E2E_REQUIRE_BINARIES` is truthy; `LOCALAI_E2E_REQUIRE_BINARIES=0` (also `off`, `no`, `n`, `disabled`, and anything `strconv.ParseBool` reads as false) forces skipping even under CI. **Any value that parses as neither reads as ON**, not off: setting the variable to something meaningless means someone meant to turn the gate on, and reading it as false would quietly restore the silent skip the flag exists to remove. The whole polarity is deliberate, because in CI a skipped cluster spec is indistinguishable from a passing one: Ginkgo exits 0 on skips. Locally a missing binary still just skips, since `CI` is unset in an ordinary shell.
- **Flake budget: no retries at all.** `--flake-attempts` is *total attempts*, not retries (ginkgo v2.29.0 `internal/group.go` sets `maxAttempts = FlakeAttempts` and loops `attempt < maxAttempts`; the flag's own usage string reads "0 - failed tests are not retried"). `DISTRIBUTED_TEST_FLAKES` defaults to **1**, so each spec runs once and a failure is a failure, and `test-e2e-cluster` pins `--flake-attempts 1` outright rather than reading the variable. The repo-wide `TEST_FLAKES=5` means up to five attempts, so up to four retries. These suites exist to surface nondeterminism, and a retry converts exactly that signal into a green run. Raise it locally when bisecting something unrelated, not in the Makefile.
- **Coverage:** `tests/e2e/distributed` is excluded from the coverage roots (`COVERAGE_E2E_ROOTS = ./tests/e2e`, run non-recursively), and so is the `cluster` helper package beneath it. Neither suite moves the baseline, so production code that these suites are the only cover for reads as **uncovered**. Unit tests for such code belong under `./core/...` with `testutil.SetupTestDB()`.
- **The cluster job builds against a stubbed React UI.** `core/http/react-ui/dist` is gitignored and built by Node, so the workflow writes a one-line `index.html` there to satisfy the `//go:embed react-ui/dist/*` in `core/http/app.go` and skips a full Node and Vite install. That holds only while the suite drives the HTTP API and never the UI, which has its own e2e suite. A spec that ever asserts on a UI asset would pass locally, where a real `dist/` exists, and be served the stub in CI: if you write one, the stub step has to go and the real build come back.
- **The topology that matters is two frontends and two workers.** `cluster_busless_test.go` runs the whole programme on it, and it is the only shape in which the OWNER path and the RELAY path are live at the same time: with the two workers' tunnels landing on different replicas, every request either goes to the replica holding that worker's tunnel or is relayed through the one that does. A one-worker cluster cannot tell those apart, and cannot state that killing a replica re-homes only ITS worker. Which worker served a request is read back from `node_models` rather than assumed, and the two models are pinned to one worker each through the admin API (`PUT /api/nodes/:id/labels` plus `POST /api/nodes/scheduling`), so a selector that failed open reddens the spec instead of hiding in it.
- **The fan-out carrier is proven under real binaries by `cluster_fanout_test.go`.** Both specs run two frontends and no workers against one PostgreSQL, publish at frontend 0 and read at frontend 1, and cost 7.0s together. They drive a gallery operation on purpose: `GET /models/jobs/<id>` answers from `galleryop`'s in-memory statuses map, which on a peer replica is filled by the `gallery.*.progress` subscriber and by nothing else (`Hydrate` runs once at startup, long before these operations exist), so a peer that answers was told over the carrier. Every other broadcast family has a durable table behind it that a peer would converge through anyway, and a spec on one of those cannot separate "the broadcast arrived" from "the row was read". The specs make that checkable rather than argued: the `gallery_operations` row is written when the gallery worker DEQUEUES an operation, so an operation still queued has none, and both specs assert zero rows while the peer is already answering with the operation's own bytes. Holding the queue is what `cluster.Options.Galleries` is for: it points every frontend at one gated index and turns the startup estimate warmer off, so the operation the spec submits is parked inside a fetch only the spec can complete. The second spec adds the oversized path: an element name of 9000 bytes puts the encoded notification past PostgreSQL's 8000-byte cap (exclusive), so the broadcast becomes a `bus_messages` row plus a notification carrying its id, and the spec asserts the row exists, decodes it, and compares its element name byte for byte with what frontend 1 answers. A one-byte control operation in the same run is asserted to leave NO row, which is what keeps "a row exists" a statement about size rather than one that is true of every operation.
- **Do not shorten the cluster suite's waits.** Three of its six specs sit at ~167s each because they wait out a 60s staleness threshold plus a 15s health-check tick. That wait is what stops the assertions from passing before the system could have reacted, which was a real false green earlier on. If the job has to get faster, the levers are CI concurrency or making the thresholds configurable, not shorter waits.
4 changes: 2 additions & 2 deletions .agents/ci-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ This is worth more than it looks. Measured over the week to 2026-07-30, **97% of

The volume is real: 13 gallery-only PRs merged that week with 10 open at once, and 78 of the 137 PRs opened were bot-generated.

`paths-ignore` on the PR trigger of `image-pr.yml` (7 jobs), `build-test.yaml` (3), `lint.yml` (2) and `tests-e2e.yml` (1) drops 13 of those 20. The excluded set:
`paths-ignore` on the PR trigger of `image-pr.yml` (7 jobs), `build-test.yaml` (3), `lint.yml` (2) and `tests-e2e.yml` (1) drops 13 of those 20, measured before `tests-e2e-distributed.yml` (2 jobs) landed. That workflow carries the same exclusion set for the same reason: its dependency graph is 99 packages, so an allowlist of paths would silently stop guarding the moment code moved, while a diff confined to the paths below provably cannot reach it. The excluded set:

| Path | Why no image or Go build can see it |
|---|---|
Expand Down Expand Up @@ -192,7 +192,7 @@ What still runs, and why it has to:
Two properties this relies on:

- `paths-ignore` skips a run only when **every** changed file matches, so a PR touching the gallery *and* Go code still runs everything. That is what makes the exclusion safe rather than a hole.
- `master` carries no branch protection and no rulesets, so a skipped workflow reports no status and nothing waits on it. If required status checks are ever introduced, these four entries must be excluded from the required set or PRs will hang on "Expected — Waiting for status to be reported".
- `master` carries no branch protection and no rulesets, so a skipped workflow reports no status and nothing waits on it. If required status checks are ever introduced, these five entries must be excluded from the required set or PRs will hang on "Expected — Waiting for status to be reported".

### `image.yml` on master push is gated too, by a job rather than a path filter

Expand Down
2 changes: 1 addition & 1 deletion .agents/localai-assistant-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,4 @@ The user chose KISS. Every mutating tool has a safety rule (`prompts/10_safety.m

## Distributed mode

The in-memory MCP server runs only on the head node (where the chat handler runs). `inproc.Client` wraps services that are already distributed-aware (`GalleryService` coordinates with workers; `ListNodes` reads the NATS-populated registry). No NATS routing of MCP tools — the admin surface lives on the head, period.
The in-memory MCP server runs only on the head node (where the chat handler runs). `inproc.Client` wraps services that are already distributed-aware (`GalleryService` coordinates with workers; `ListNodes` reads the PostgreSQL-backed node registry). MCP tools are not routed anywhere: the admin surface lives on the head, period.
Loading
Loading