`claim_job` took an `organization_id` and matched it against the job's. Read plainly that says: a machine may only do work for the tenant that owns it. Both paired nodes belong to BDMurphy and The Last Dwarf belongs to WorstAuthor, so a node could never do a word of the audio node's work however idle it was. The only way to have found that out was to queue the job and watch it sit there. `place()` had the same fault - it listed candidate nodes by organization - so the scheduler could not use the fleet across tenants either. Both are fixed. A node is a machine. It belongs to whoever administers it, not to a tenant, and a tenant's data reaches it inside one job's payload for the duration of that job, never by widening what the node itself can see. `kind` and the placement requirements decide what a node may do; ownership decides who may switch it off. What did not change: a node still returns one job's payload and nothing else, so it still cannot enumerate another tenant's work. test_node_tenancy.py asserts the behaviour, and was confirmed to detect the old one: five of its seven tests fail when the tenant filter is restored. Also adds docs/plan-translation-pipeline.md, which separates the critical path - the work-function protocol, TTS, assembly, lineage, and one finished episode of The Last Dwarf - from the work that can proceed beside it. Writing those tests cost more time than it should have and the reason is worth recording: four of them failed for reasons that had nothing to do with tenancy. A job left behind by one test is claimed by the next, which reads as the rule breaking. `TRUNCATE ... CASCADE` on a table shared with other fixtures destroyed `project_publishers` and surfaced two files away as "relation does not exist". The queue is now left alone and the claims filter on a job kind only this file creates, so a neighbour's leftovers cannot be returned and the assertions can be exact. 547 passed, 29 skipped.
6.7 KiB
Plan: from nodes to a finished translation of The Last Dwarf
Written 2026-10-03. The goal is not a well-architected system; it is a finished translation of The Last Dwarf, produced by nodes, from scratch.
This plan separates what is on the critical path from what can run beside it, because they do not block each other and pretending otherwise would serialise work that need not be serialised.
What "finished" means here
A person should be able to listen to a complete episode of The Last Dwarf in a language of their choosing, and it should have been produced by this system rather than assembled by hand. That is the test. Anything that does not move that number is not on this plan.
The pipeline has three stages, and each is a separate kind of work:
| stage | input | output | node kind |
|---|---|---|---|
| translate | source text | translated text | LLM |
| speak | translated text | audio per line | TTS |
| assemble | audio per line | one playable file | processing |
Node 238 can do speak and assemble today: 12 cores, 47 GiB RAM, no GPU. CPU TTS is slower than a GPU but it is real, and it proves the path. Translate needs a model that does not exist on this hardware yet, and is the one stage with an open dependency.
Track A — the critical path
Nothing here can be backgrounded, because each step is the input to the next and each needs a decision from you.
A1. Work functions — what a node can actually be asked to do
The agent currently implements exactly one work function: sleep. Everything else is
absent, so there is nothing to dispatch. Three work functions are needed, and the
protocol matters more than the three implementations:
- What a job declares it needs (a model, a device, a minimum size).
- What it returns (an artifact reference, not bytes — bytes never transit the controller).
- What it refuses, and how.
A work function that cannot refuse is how a node ends up claiming work it will fail.
Done when: a job of each of the three kinds can be claimed by a node, run, and reported, with refusal paths tested.
A2. speak — TTS on node 238
The first real capability. Deliberately first because it needs no model download and no GPU, so it can be finished and heard.
The audio node already holds the translated text and the voice decisions — 11 tracks,
461 speaker rows, speaker_name free text with three-way count disagreements. What it
does not have is a way to say a line out loud.
Open decision for you: which TTS engine. Piper is CPU-only and fast enough; its output quality is adequate for proof. XTTS and the neural options are better and need either a GPU or a slower CPU path. This is worth your ear before I build it, because "it works" and "it is listenable" are different bars for the thing you are trying to deliver.
A3. assemble — the final product
Per-line audio becomes one episode: normalise loudness, concatenate in cue order, write metadata. This is where "the system produced a translation" becomes demonstrable rather than asserted.
This is the stage you named, and it is the one that makes the other two worth having.
A4. Artifact lineage
artifacts and job_artifacts exist (migration 0003 §5) but nothing writes to them.
The chain — source text → translation → per-line audio → assembled episode — has to be
recorded as it happens, or the pipeline produces files nobody can trace.
A5. The last dwarf, end to end
One episode, start to finish, no manual steps. The deliverable is the file.
Track B — parallel, does not block Track A
B1. Schema-per-tenant migration
Fully specified in Umbratools_2.md §5.0. It touches 8 tables, 19 query sites, and the
tenancy test suite — none of which the compute path reads. It can run to completion
while Track A proceeds.
Constraint: it must not touch compute_nodes, jobs, node_credentials or
node_heartbeats. Those are platform-level, not tenant-level — a node is not owned by
an organization, it serves organizations. Putting them in a tenant schema would mean a
node in organization A cannot be seen by the controller. They stay in public, and that
is a deliberate exception rather than an oversight.
Risk: rewriting test_rls_tenancy.py while Track A is changing the compute surface
means two large diffs in flight at once. This is the argument for sequencing them, and it
is the one real argument against full parallelism.
B2. Capability benchmarking
The probe now measures instruction sets by executing them, on the principle that a
declared capability is not a measured one. What it does not yet do is produce a
throughput_factor from a measurement rather than a string match on a GPU model
name.
On 238 this is tractable: 12 cores, no GPU, and the numbers are honest. The string-matched factor stays on the GPU path until there is hardware to measure.
B3. Ingest API and object storage
How translated text and audio reach the system at all. Independent of who computes on it. Blocked on TLS, which is a prerequisite for any write path.
The two dependencies that are not mine
TLS in front of the deployment. Caddy is inactive and the Caddyfile is empty. Nothing should be written to this system over plaintext HTTP on a network others can reach. This blocks B3 and should not block A1–A3, all of which are compute-side.
The audio node's own work. It has production translation work in flight. Track A needs nothing from it until A5, and A5 needs the translated text — so the audio node's schedule is a real constraint on the finish date, and I do not control it.
What I would do first, and why
A1, the work-function protocol, before any TTS engine. It is the smallest piece that
makes the other three possible, and building it against sleep means the TTS work
function is written once against a protocol that has already been tested rather than
being the thing that discovers the protocol's gaps.
The failure I would most want to avoid: a TTS function that works, and a protocol discovered afterwards to have no way to express "this node cannot do that" — which is exactly how a heterogeneous fleet ends up silently failing work it cannot perform.
State at writing
- Two nodes paired (
win-test-238,umbraaudio-m4), one retired test node. - Bootstrap works with and without root; the root branch is untested — needs a sudo password on 238.
- Capability measured by execution on both machines. On 238 the hypervisor presents a
CPU that does not exist (
Ryzen 9 3900X, faults on AVX2), which is now detected and reported rather than believed. - 525 tests pass. Two live defects: gate G2 (the application connects as table owner)
and
VIEWSinweb/assets/admin.jsomittingprojects.