- bruker_ikke_workspace: RLS-advarsel tatt på alvor — samlings-noder er harde sikkerhetsiloer under panseret, edge-basert tilgang er UX-lag innenfor siloene - datalaget: AGE moderert fra "beslutning" til "planlagt utvidelse" — start med CTEs, legg til AGE når det faktisk trengs - maskinrommet: compute-separasjon dokumentert — tunge workers kan flyttes til egen node, maskinrommet ruter transparent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
322 lines
14 KiB
Markdown
322 lines
14 KiB
Markdown
# Maskinrommet — teknisk tjenestelaget
|
|
|
|
> Én Rust-tjeneste med et fast grensesnitt. Alle tekniske tjenester beveger
|
|
> seg gjennom dette laget. Fang, prosesser, lever.
|
|
|
|
## Observasjoner
|
|
|
|
I dag er tekniske tjenester spredt:
|
|
- **Worker** (Rust) — kjører bakgrunnsjobber
|
|
- **Jobbkø** (PG) — koordinerer arbeid
|
|
- **AI Gateway** (LiteLLM) — ruter AI-kall
|
|
- **Whisper** — transkripsjon
|
|
- **LiveKit** — lyd/video-strømmer
|
|
|
|
Hver har sitt eget grensesnitt. Frontend og primitiv-laget må vite hva
|
|
som finnes under panseret. Det er ingen felles abstraksjon, ingen felles
|
|
logging, ingen felles kapasitetsstyring.
|
|
|
|
## Tesen
|
|
|
|
Alt som krever tunge ressurser eller eksterne tjenester går gjennom
|
|
**ett lag** med **ett grensesnitt**. Ikke fordi det er elegant — fordi
|
|
det gir et fast punkt som er enkelt å fange, modifisere og forbedre.
|
|
|
|
Maskinrommet gjør tre ting:
|
|
|
|
### 1. Fang (input-absorpsjon)
|
|
Ta imot råmateriale i alle modaliteter:
|
|
- Tekst (melding, URL, dokument)
|
|
- Lyd (voice memo, live stream, filopplasting)
|
|
- Bilde (foto, skjermbilde, tegning)
|
|
- Video (stream, opptak)
|
|
- Strukturert data (JSON, metadata, edges)
|
|
|
|
### 2. Prosesser (transformasjon)
|
|
Analyser, transformer, berik og systematiser:
|
|
- **STT** — lyd → tekst (Whisper)
|
|
- **TTS** — tekst → lyd (ElevenLabs / lokal modell)
|
|
- **AI-analyse** — oppsummering, klassifisering, sentimentanalyse,
|
|
faktasjekk, edge-forslag
|
|
- **Beriking** — URL → metadata, bilde → beskrivelse, lyd → segmenter
|
|
- **Søk** — fulltekst, semantisk (pgvector), graftraversering
|
|
- **Mediaprosessering** — transcode, thumbnail, waveform
|
|
|
|
### 3. Lever (output-distribusjon)
|
|
Lever resultat i riktig modalitet til riktig mottaker:
|
|
- Tekst (melding, notifikasjon, digest)
|
|
- Lyd (TTS-opplesning, lydstream)
|
|
- Video/bilde (stream, thumbnail, snapshot)
|
|
- Strukturert data (noder, edges, metadata tilbake i grafen)
|
|
- Push (webhook, SSE, SpacetimeDB-reducer)
|
|
|
|
## Edge-drevet ressursorkestrering
|
|
|
|
Nøkkelinnsikten: **maskinrommet leser edges for å vite hva det skal gjøre.**
|
|
Noden selv er alltid enkel. Det er edgene som bestemmer hvilke ressurser
|
|
som spinnes opp.
|
|
|
|
### Security by default
|
|
Input uten mottaker-edge er automatisk privat. Du trenger ikke "velge
|
|
privat" — det er utgangspunktet. Ingen ser det. Ingen ressurser kobles
|
|
inn utover det grunnleggende (fang + transkriber). Privat er ikke en
|
|
innstilling, det er fravær av deling.
|
|
|
|
### Ressurser er proporsjonale med edges
|
|
Samme nodetype, vilt forskjellig ressursbruk:
|
|
|
|
```
|
|
Dagboknotat (privat voice memo):
|
|
node → fang lyd → transkriber (Whisper) → lagre
|
|
Ressurser: minimal
|
|
|
|
Samtale med Trond:
|
|
node + mottaker-edge(Trond)
|
|
→ fang lyd → transkriber → lever tekst/lyd til Trond
|
|
Ressurser: STT + levering til én
|
|
|
|
Redaksjonsmøte (5 deltakere):
|
|
node + mottaker-edges(5) + rolle-edges
|
|
→ fang lyd fra alle → transkriber → lever til alle → AI-referent
|
|
Ressurser: STT + levering til 5 + LLM
|
|
|
|
Livesending (1000 lyttere):
|
|
node + mottaker-edges(∞) + stream-edge + publiserings-edge
|
|
→ fang lyd → transkriber → stream via LiveKit → distribuer
|
|
→ generer segmenter → kjør live AI → publiser
|
|
Ressurser: STT + LiveKit + LLM + mediaprosessering
|
|
```
|
|
|
|
Maskinrommet gjør ikke mer enn det edges krever. Ingen overhead for
|
|
enkle ting. Noden vet ingenting om LiveKit — den har bare edges som
|
|
sier "stream til disse mottakerne", og maskinrommet bestemmer at det
|
|
betyr LiveKit.
|
|
|
|
### Naturlig eskalering
|
|
Du starter en privat voice-note. Bestemmer deg for å dele den med Trond
|
|
→ legg til mottaker-edge, maskinrommet begynner å levere. Trond foreslår
|
|
at dere tar det som et møte → legg til flere deltaker-edges, maskinrommet
|
|
kobler inn sanntidsstrømming. Møtet blir en innspilling → legg til
|
|
publiserings-edge, maskinrommet aktiverer produksjonspipeline.
|
|
|
|
Hvert steg er bare å legge til edges. Maskinrommet reagerer og kobler
|
|
inn flere ressurser etter hvert. Ingen migrering, ingen modebytte.
|
|
|
|
## Grensesnittet
|
|
|
|
Maskinrommet eksponerer et konsistent API — sannsynligvis en Rust trait
|
|
eller et sett traits:
|
|
|
|
```
|
|
fang(input: RåInput) → NodeId
|
|
prosesser(node: NodeId, operasjon: Operasjon) → Resultat
|
|
lever(node: NodeId, mottaker: Mottaker, format: Format) → Status
|
|
```
|
|
|
|
Men i praksis er mye av dette *reaktivt*: maskinrommet observerer
|
|
edge-endringer og handler automatisk. Legger noen til en mottaker-edge
|
|
→ maskinrommet begynner å levere. Legger noen til en stream-edge →
|
|
maskinrommet kobler inn LiveKit. Primitivene trenger ikke eksplisitt
|
|
kalle `lever()` — de manipulerer edges, og maskinrommet reagerer.
|
|
|
|
## Hva dette gir
|
|
|
|
### Isolasjon
|
|
Bytt Whisper med noe annet? Endre maskinrommet. Frontend vet ingenting.
|
|
Legg til bildegenerering? Ny operasjon i maskinrommet. Primitivene
|
|
kaller den uten å vite hva som skjer under.
|
|
|
|
### Observerbarhet
|
|
Alt går gjennom ett punkt. Logging, metrikker, kostnadsrapportering,
|
|
feilhåndtering — alt på ett sted. "Hva bruker vi AI-ressurser på?"
|
|
har ett svar.
|
|
|
|
### Kapasitetsstyring
|
|
Prioritering, kø, rate limiting, fallback mellom leverandører — alt
|
|
håndtert av maskinrommet. En podcastinnspilling som trenger live
|
|
transkripsjon kan prioriteres over en bakgrunns-oppsummering.
|
|
|
|
### Fast utviklingspunkt
|
|
To team (eller to hatter) med klart grensesnitt:
|
|
- **Over maskinrommet:** primitiver, noder, edges, UI, brukeropplevelse
|
|
- **I maskinrommet:** ytelse, integrasjoner, kapasitet, kostnad
|
|
|
|
Du kan perfeksjonere det ene uten å røre det andre.
|
|
|
|
## Content-Addressable Storage og intelligent pruning
|
|
|
|
Maskinrommet forvalter også lagring. Ikke alt kan lagres for evig — men
|
|
ikke alt trenger det heller. Signalene for hva som er viktig finnes
|
|
allerede i grafen.
|
|
|
|
### CAS som lagringsprimitiv
|
|
All binærdata (lyd, bilde, video) lagres i et content-addressable store.
|
|
Fordeler:
|
|
- **Deduplisering gratis** — samme fil delt i tre kontekster = én kopi
|
|
- **Separasjon** — "innholdet eksisterer" er adskilt fra "innholdet er
|
|
tilgjengelig." Noden peker på en hash, CAS har filen (eller ikke).
|
|
- **Enkel opprydning** — slett hashen fra CAS, alle noder som pekte
|
|
dit mister binærdataen men beholder metadata og transkripsjon.
|
|
|
|
### Lagringsregler per modalitet
|
|
|
|
| Modalitet | Default levetid | Begrunnelse |
|
|
|-----------|----------------|-------------|
|
|
| Tekst | Evig | Billig, er essensen av innholdet |
|
|
| Transkripsjon | Evig | Tekstlig representasjon av lyd/video — tar vare på meningen |
|
|
| Lyd | 30 dager | Mellomkostnad, transkripsjon bevarer innholdet |
|
|
| Bilde | 30 dager | Mellomkostnad, beskrivelse/metadata bevarer kontekst |
|
|
| Video | 14 dager | Dyrest, transkripsjon + thumbnail bevarer det meste |
|
|
|
|
### Signaler som forlenger levetid
|
|
|
|
Default-TTL er bare utgangspunktet. Maskinrommet justerer basert på:
|
|
|
|
- **Edges.** En lydfil med edge til episoderegisteret = publisert
|
|
podcast, beholdes. En privat voice-memo uten edges = 30-dagers TTL.
|
|
- **Aksesslog.** Hvis noen har spilt av lydfilen i løpet av TTL-perioden,
|
|
forlenges den. Ingen aksess = ingen verdi i å beholde binærdataen.
|
|
- **Transkripsjonsstatus.** Lyd som er transkribert har "overlevert sin
|
|
essens" til tekst. Lyd som *ikke* er transkribert (f.eks. musikk,
|
|
lydeffekter) kan trenge lengre TTL.
|
|
- **Edge-type.** Edge til publisert innhold = behold. Edge til arkivert
|
|
møte = transkripsjon holder. Edge til ingenting = teksten lever videre,
|
|
binærdataen kan dø.
|
|
|
|
### Eksempler
|
|
|
|
```
|
|
Privat voice-memo, aldri delt:
|
|
→ Lyd transkriberes → tekst lagres evig
|
|
→ Lydfil: 30 dager, ingen aksess, ingen edges → slettes
|
|
→ Noden lever videre med teksten
|
|
|
|
Podcastepisode:
|
|
→ Lyd har edge til episoderegister + publiserings-edge
|
|
→ Aksesseres regelmessig via podcastarkivet
|
|
→ Lydfil: beholdes så lenge edges og aksess tilsier det
|
|
|
|
Rutinemøte for et år siden:
|
|
→ Video (6 kanaler): ingen har sett den på 6 måneder → slettes
|
|
→ Lyd: ingen har spilt av → slettes
|
|
→ Transkripsjon: tekst, lagres evig. Søkbar, refererbar.
|
|
→ Noden lever med full kontekst minus binærdata
|
|
|
|
Viktig styremøte:
|
|
→ Video aksesseres av styremedlemmer → forlenges
|
|
→ Workspace-innstilling: "behold video i 1 år" → overrider default
|
|
```
|
|
|
|
### Generert innhold er en cache
|
|
TTS, thumbnails, AI-oppsummeringer, waveforms — alt som kan regenereres
|
|
fra kildedata er i praksis en cache. Det lagres i CAS med samme TTL-
|
|
mekanisme som alt annet:
|
|
- Peter ber om lyd-versjon av en tekstmelding → TTS genereres, lagres
|
|
- Ingen spiller den av på 30 dager → filen slettes fra CAS
|
|
- Peter (eller noen andre) ber om lyd igjen → regenereres on-demand
|
|
- Teksten er der alltid. Binærdataen er flyktig.
|
|
|
|
Maskinrommet trenger ikke skille mellom "original lyd" (voice memo) og
|
|
"generert lyd" (TTS) i pruning-logikken. Begge er binærdata i CAS med
|
|
en TTL som forlenges ved aksess. Forskjellen er bare at generert
|
|
innhold alltid kan gjenskapes fra kilden — så det er tryggere å prune.
|
|
|
|
### Workspace-styrt aggressivitet
|
|
Hvert workspace kan justere sin pruning-profil:
|
|
- **Konservativt** — behold alt lenge (f.eks. arkiv-workspace)
|
|
- **Aggressivt** — tekst bevares, binærdata prunes raskt (f.eks.
|
|
daglig drift-workspace med mye rutineinnhold)
|
|
- **Tilpasset** — egne regler per modalitet og edge-type
|
|
|
|
### Brukerens erfaringsbaserte meny
|
|
Over tid bruker du noen edges oftere enn andre, noen noder oftere enn
|
|
andre. Maskinrommet observerer dette og tilbyr en erfaringsbasert meny:
|
|
dine mest brukte koblinger, dine vanligste input-mønstre, dine
|
|
foretrukne modaliteter. Ikke som en rigid konfigurasjon — som en
|
|
adaptiv overflate du kan aktivere og deaktivere fortløpende.
|
|
|
|
Dette er ikke maskinlæring eller kompleks AI — det er frekvenstelling
|
|
på edges og aksesslog. Enkelt å implementere, intuitivt for brukeren.
|
|
|
|
## Pragmatisk vei dit
|
|
|
|
Ikke bygg dette fra scratch. Formaliser det som allerede finnes:
|
|
|
|
1. **Worker + jobbkø er allerede kjernen.** De trenger et konsistent
|
|
API, ikke en omskriving.
|
|
2. **AI Gateway (LiteLLM) absorberes** — i stedet for en separat proxy,
|
|
blir LLM-kall en operasjon i maskinrommet som alt annet.
|
|
3. **Whisper, TTS, mediaprosessering** — allerede planlagt som
|
|
worker-jobber. Gi dem samme grensesnitt.
|
|
4. **LiveKit** — den mest spesielle tjenesten (sanntidsstrømmer). Kan
|
|
starte som en separat integrasjon og formaliseres inn over tid.
|
|
|
|
Rekkefølge: definer traits → migrer eksisterende worker-jobber inn →
|
|
legg til nye tjenester etter hvert. Fast punkt fra dag én, full
|
|
dekning over tid.
|
|
|
|
## Compute-separasjon
|
|
|
|
Maskinrommet orkestrerer — men tunge jobber trenger ikke kjøre på
|
|
samme maskin. Hetzner CPX42 (8 vCPU, 16 GB RAM) skal håndtere state
|
|
(PG, SpacetimeDB) og sanntid (Caddy, LiveKit, SvelteKit). Whisper
|
|
(CPU-intensiv, spesielt large-v3) og lokal LLM (kildevern-modus)
|
|
vil konkurrere om ressurser under live innspilling.
|
|
|
|
Maskinrommets abstraksjon gjør dette løsbart:
|
|
- **Nå:** Alt på én VPS. Jobbkøen prioriterer sanntid over batch.
|
|
Whisper kjøres med lavere concurrency under live-sesjoner.
|
|
- **Senere:** Trekk ut tunge workers til en separat node (billig
|
|
ARM/Ampere-instans) som poller jobbkøen over internt nettverk.
|
|
Maskinrommet ruter transparent — primitivene merker ingenting.
|
|
- **Kildevern-modus:** Lokal LLM (Llama/Gemma) krever GPU eller
|
|
dedikert compute. Urealistisk på delt VPS. Egen node for dette.
|
|
|
|
Poenget: maskinrommet er designet for å rute arbeid, ikke for å
|
|
*utføre* alt selv. Compute-separasjon er en konfigurasjon, ikke en
|
|
arkitekturendring.
|
|
|
|
## Spenninger og åpne spørsmål
|
|
|
|
- **Synkron vs asynkron.** "Fang" og "lever" kan være instant, men
|
|
"prosesser" kan ta sekunder (TTS) eller minutter (full episode-
|
|
transkripsjon). Grensesnittet må håndtere begge naturlig.
|
|
- **Strømmer.** Live lyd/video er fundamentalt annerledes enn
|
|
request/response. Men edge-modellen løser mye: maskinrommet ser en
|
|
stream-edge og vet at det betyr LiveKit. Utfordringen er *reaktivitet*
|
|
— maskinrommet må observere edge-endringer i sanntid og koble inn/ut
|
|
ressurser dynamisk.
|
|
- **Granularitet.** Hvor mye skal maskinrommet vite om domenet? "Fang
|
|
lyd" er generisk, men "transkriber og splitt i segmenter med
|
|
taler-identifikasjon" er domenespesifikt. Hvor går grensen?
|
|
- **Overhead.** Et ekstra lag betyr et ekstra kall. For tunge
|
|
operasjoner (Whisper, LLM) er det neglisjerbart. For lette
|
|
operasjoner (slå opp metadata) kan det være unødvendig indirection.
|
|
|
|
## Plassering i lagmodellen
|
|
|
|
Maskinrommet (Rust) er det eneste orkestringslaget. Alle tjenester —
|
|
inkludert PG, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit — er
|
|
likeverdige tjenester *under* maskinrommet. SpacetimeDB er ikke et lag
|
|
mellom Rust og GUI, det er en tjeneste maskinrommet skriver til.
|
|
|
|
Ett unntak: SpacetimeDB har en direkte WebSocket-kobling til frontend
|
|
for sanntids lese-strøm. Dette er en bevisst optimering — STDB sitt
|
|
klient-SDK gir ~10μs-oppdateringer med automatisk synk og lokal cache.
|
|
Å proxy dette gjennom Rust ville vært å bygge en dårligere versjon av
|
|
noe STDB gjør optimalt.
|
|
|
|
Se [datalaget](datalaget.md) for full lagmodell med diagram.
|
|
|
|
## Forhold til andre retninger
|
|
|
|
Maskinrommet er infrastrukturen *under* de tre primitivene i
|
|
[universell input og mottak](universell_input.md):
|
|
- Input-primitiven kaller `fang()` + `prosesser()`
|
|
- Mottak-primitiven kaller `lever()`
|
|
- Kommunikasjonsnoden bruker alle tre (fang input fra deltakere,
|
|
prosesser sanntid, lever til mottakere)
|
|
|
|
Det er også det som gjør to-lags-modellen fra [rom, ikke forum](rom_ikke_forum.md)
|
|
praktisk: maskinrommet ruter til riktig lag (sanntid vs tradisjonelt)
|
|
uten at primitivene trenger å vite forskjellen.
|