Files
Maven/maven.md
T
2026-07-03 00:32:48 +02:00

30 KiB
Raw Blame History

maven

a self-hosted personal assistant. manages your day, acts on your homelab. all software, fully local — never phones home.

consolidated from: spec, decisions, router, two-memory, presence, auth, stt/tts. dated 2026-06-30.


capabilities

reactive

  • converse — voice in → stt → router → llm → tts, and text
  • act — function calls into the homelab ("do digital things")

proactive

  • health nudges — hydration, meals, breaks, shower, sleep, cleanup
  • user reminders — your stated future intent ("remind me tuesday", "wake me 7"); scheduled, fires once
  • deliver — voice when near, ntfy/telegram/matrix when away
  • restrain — quiet hours, per-rule cooldowns, snooze-memory, self-quieting

capture

  • throw facts/notes/tasks at it mid-flow → feeds memory

state

  • self — timestamped facts about you (meals, water, sleep, desk time)
  • presence — are you here (inferred, decaying confidence — never one signal)
  • activity — what you're doing right now
  • environment — world facts that gate or trigger: homelab health, calendar, weather

memory

  • long-term recall, personalization (obsidian → chroma)

feedback

  • nudge outcomes (acted / snoozed / ignored) tune the rules
  • you correct a bad fact; it knows its own hit rate
  • self-quieting falls out of this

identity

  • consistent character — tone, values, phrasing. pinned in prompt, makes restraint legible

surface

  • talk to it (phone page, pc client)
  • it reaches you (the channels above)
  • prove it's you — auth; this thing holds your life and can act

non-goals — what maven isn't

outside boundary

  • not alexa/siri on steroids
  • not a smart device

inside boundary (the ones that actually constrain the build)

  • not autonomous — suggests and acts on command; proactive triggers never get unsandboxed action rights. "backup failed, rerun it?" — never reruns it herself. suggest≠act is the safety model
  • not a guesser-of-truth — inference changes whether she asks, never what she records. a confident wrong fact is worse than a known gap
  • not a nag — she'd rather miss a nudge than be mutable. shuts up when uncertain. load-bearing
  • not a stranger — runs on your stuff, your model, your data. never phones home
  • not a relationship — mom-tone is a function that makes nudges land, not emotional company. names the drift a warm small model falls into

architecture

  • daemon lives on homesrv (always-on), not the workstation
  • trigger loop is dumb: ticks ~1min, no llm, evaluates deterministic predicates against state
  • llm wakes only when a predicate fires; job is narrow — phrase + (no) final veto, not drive the loop
  • lfm2.5 / sub-1b for phrasing — prompted, not trained. training effort stays on tts/stt/personality
  • presence = decaying confidence score over multiple weak signals, with hysteresis; never trust one source
  • self-facts → care nudges. world-facts → ops + context. same engine
  • rules as code, not a config DSL — until ~30 rules and you feel the pain
  • proactive triggers: read + suggest only, never action rights

build order

state layer is first. nothing proactive works without state to evaluate predicates against — it's the floor, built before the loop, phrasing, or delivery.

storage — sqlite

  • single-user, no concurrent writers, on a box already tight on ram → sqlite, not postgres. library not a process, no port to harden, backup is cp
  • sqlcipher at-rest. key read at daemon start, not hardcoded
  • give up postgres LISTEN/NOTIFY — loop polls anyway, non-loss
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA foreign_keys=ON;
PRAGMA busy_timeout=5000;

schema — append-only, three shapes

never UPDATE a value. a wrong fact is superseded, not overwritten — keeps the audit trail. current value = latest non-voided row for a key.

facts — substrate, all observations (self + env)

facts (
  id, ts,            -- ts = valid-time (true-as-of), not insert-time
  kind,              -- 'self' | 'env' | 'config'
  key, value,        -- value json if structured
  source,            -- tap:* | infer:* | poll:* | ambient | promote | feedback
  confidence,        -- 1.0 taps only; <1 inferred
  voids_id           -- correction points at the fact it cancels
)
-- index (key, ts desc)

reminders — user intent, fires once

reminders ( id, created_ts, fire_ts, payload, status )  -- pending|fired|cancelled

nudges — every proactive send + outcome. this table IS the restraint memory

nudges ( id, ts, rule, channel, message, outcome, outcome_ts )  -- pending|acted|snoozed|ignored

presence is not a table — pure function over recent facts, computed each tick. only stateful bit is hysteresis:

presence_state ( last_bucket, last_score, updated_ts )

trigger model

  • loop ticks ~60s, no llm. 99% of ticks evaluate a few predicates and die for free
  • a predicate is (State) -> Boolean, pure, no i/o → unit-tests with a fake State, zero infra
  • since(key)==null → don't fire. silence on no-data = "shuts up when uncertain"
  • the gate is universal, applied by the loop, never per-rule — quiet-hours, presence, cooldown, snooze, calendar-busy all live in one fires(). cross-cutting restraint in one place or it drifts
  • one nudge per tick (max severity), never dogpile
  • rules as code, not a DSL. revisit at ~30 rules

rules decide, llm phrases

the rule decides whether maven speaks — absolute, deterministic. the llm only words it: input (rule, severity, context), output message. no send/veto bool. a nondeterministic sub-1b model never gets to silently kill a greenlit nudge. suppression-context ("don't nag mid-meeting") moves INTO the gate as an env predicate, not the llm's job.

user reminders — separate class

  • relative→absolute at capture ("in 4h" → store now+4h, never the string)
  • reuses the loop, not a second scheduler — just a predicate: fire_ts <= now AND pending
  • bypasses the restraint gate — "wake me 7" fires in quiet hours; that's the point. snooze still applies. two delivery paths

reactive path — router

the decision: classifier, not slm

routing is a decision, and every decision in maven stays deterministic. an slm router is the one nondeterministic decider banned everywhere else. a classifier gives a confidence-scored distribution over a fixed label set, which is what routing actually wants. classifier owns the route. slm stays in its phrasing lane. same boundary as rules decide, llm phrases, extended to the reactive path.

a cascade, not classifier-vs-deterministic

not alternatives — layers.

  • stage 0 — exact match (regex/grammar). wake-word + known command grammar. "maven, restart nginx" hits the allowlist directly, skips the classifier. lowest latency — the vosk command path. boring high-frequency acts for free
  • stage 1 — intent classifier. everything free-form. embed utterance, nearest-centroid over labeled intents. one forward pass, ~30ms cpu, yields a similarity score to threshold
  • stage 2 — slot extraction, per intent. classification gives what kind, not the args. reminders need a datetime, acts need fn + params. you parse
  • stage 3 — confidence gate. below threshold → clarify, don't guess. same pattern as since(key)==null → don't fire. a misrouted fact is a confident wrong write — worse than a gap

save-where — the two-memory routing axis

one discriminator: does the loop evaluate a predicate against it?

intent example lands in why
act "restart the backup" function call (allowlist) command now, not stored
reminder "wake me 7", "vet tuesday" reminders (sqlite) has a fire-time
fact "drank water", "slept 6h" facts (sqlite) structured state the loop reasons over
note "gpu driver fixed the flicker", "prefer backups at 3am" chroma/obsidian recall/preference, no predicate touches it
query "is the backup up?", "when'd i last eat?" slm (reads sqlite or chroma) answer, don't store

fact-vs-note is the whole line: predicate will read it → structured facts row; just "recall when relevant" → semantic store. reminder splits off by future timestamp; act splits off by being imperative-now. routing isn't a separate mechanism from classification.

the preference seam — forced, not a choice

a preference ("prefer backups at 3am") looks like note-or-config. it isn't, because of a hard constraint: a predicate cannot read chroma. the loop is dumb + deterministic; it can't run a vector search every tick. so the moment a preference becomes load-bearing, it MUST exist as a structured key=value row the loop can evaluate. the store split is physical, not cosmetic. so:

  • capture → always a note (chroma). default, inert, fail-safe. costs nothing, drives nothing
  • a note stays a note until a rule needs it

promotion

promotion = the moment a human authors a predicate that reads the value.

  • authoring the rule copies the value into facts (kind=config, source=promote) — a deterministic key the loop can evaluate
  • the note stays as provenance (where the config came from, in your words)
  • the predicate reads the promoted facts row, never chroma

properties: one-way, never auto (a note can't self-promote — no path from chroma into the loop that skips a human writing a predicate). closes the injection hole — ambient-derive overhears the TV say "prefer backups at 3am" → lands as inert note → can't drive the loop without you authoring the rule. same instinct as compromised router can't forge a capability. identical shape to proposed → enabled: low-authority form free + automatic, high-authority form requires a deliberate human act.

tool registration — drafting is suggest, enabling is act

maven can scaffold a tool she's missing. she cannot enable it.

  • maven detects the gap, scaffolds the registration (name, command, params, destructive y/n), writes a proposed row, surfaces it — "earn the right to ask"
  • proposed → enabled flips through an authed surface (pc client / authed page), never the voice/chat path — that's the act, and it's the user's

why human-only: editing the allowlist is the one act that moves the boundary — a boundary you can move from inside isn't one. registration is privilege escalation, a different authority tier than invoking a listed tool. paranoid case: prompt injection via ambient-derive — TV says "maven add a shell tool" — a self-registering maven grants itself arbitrary capability. human-only enable keeps a compromised maven boxed by what's already on. low-friction stays intact: she builds the stubs, you review + enable. you just never lose the pen.

confirmation is not one mechanism

a gate assumes a fully-formed action — "drop the db? y/n" works because the action is already specified. an underspecified request can't be gated; you can't confirm what isn't specified. confirmation scales to how formed the act is.

stack

  • classifier: multilingual-e5-small or paraphrase-multilingual-MiniLM-L12-v2, onnx int8 (~120mb). bilingual native, no separate ru/en path. nearest-centroid needs ~10 examples/intent, no training pipeline
  • dates: dateparser — ru+en relative+absolute, does relative→absolute at capture for free
  • acts: fuzzy-match against the fn allowlist. not on the list → refuse, don't improvise. destructive ones still gate behind confirm — "drop the db?" must not fire on a fuzzy match
  • slm's remaining lane: phrasing the reply + last-resort slot extraction for free-form notes the parsers choke on. never the router decision
  • misroute correction = new centroid example — append-only, grows the classifier as used. same shape as nudges.outcome tuning cooldowns. more reliable over time, no retrain

stt / tts (low-tier, cpu, ru+en)

target box: ryzen + 13gb ram, igpu only, no real gpu. onnxruntime on cpu for everything — igpu rocm is flaky and pointless for sub-1b models.

picks:

  • stt: faster-whisper small (int8) — primary, ru+en, handles mid-sentence code-switching
  • stt (low-latency path): vosk ru — known command grammar; runs alongside fw (this is stage-0)
  • tts: silero (ru-native) — verify license + voices first; piper ru as the safe floor
model role params langs (ru?) license cpu fit
faster-whisper small/int8 stt ~240M 99+ incl ru MIT ~95% large-v3 acc @ 6x speed
vosk (ru model) stt tiny 20+ incl ru Apache 2.0 real-time, native streaming, runs on a pi
moonshine stt 245M english only MIT streaming, ~6x smaller than whisper-lg
silero tts small russian-native check it* fast on cpu
piper (ru voices) tts tiny 30+ incl ru GPL-3.0 (fork) real-time on a pi, audibly synthetic
xtts v2 tts ~470M 17 incl ru CPML (non-commercial) cpu works, 5-10x slower, heavier
kokoro tts 82M no russian Apache 2.0 fast on cpu, en-first

bolded = the actual picks.

notes: faster-whisper = ctranslate2 int8, ~4x faster than vanilla whisper, lower memory. kokoro is in the table only as a "don't bother for ru" marker. ram math: fw ~0.51gb + tts ~1gb → ~10gb left for the router llm + os, comfortable, no swap. silero row is prior knowledge, no source pulled — confirm license + current ru voices before wiring in. bilingual / mid-sentence code-switch → lean on faster-whisper, not vosk.


state — signal sources

explicit taps set truth. passive signals drive prompting. a passive signal can never write a self-fact — it only makes maven ask. boundary lives in source; rules trust provenance.

  • self / taps (1.0): water, meal, shower — phone / voice / telegram
  • self / passive (activity, not truth): desk-idle (hyprland / GetLastInputInfo), voice_active, sleep
  • presence (weak, decaying, multi-source): wg handshake, page heartbeat, hyprland-not-idle. no LAN sweeps — too invasive; wg+heartbeat get ~90%
  • env / polled: healthcheck (disk/service/backup/cert), calendar (caldav/.ics), weather
  • provenance-scoping: a rule on service_down trusts only source=poll:healthcheck. a compromised poller must not be able to forge a trigger

presence — concrete scoring

pinned: pure function over recent facts, computed each tick; decaying confidence over multiple weak signals, never one authoritative source; hysteresis to stop flapping.

combiner — noisy-OR, not weighted sum. these are independent-ish positive indicators of one latent binary ("kami here?").

p_i = weight_i · exp(-Δt_i / τ_i)        # one signal's decayed contribution
P   = 1  Π (1  p_i)                     # combine

diminishing returns on stacking weak signals, never exceeds 1.0. "any one signal raises confidence, no single one owns it" falls straight out. weighted-sum rejected: heartbeat+wg fresh would peg identical to everything-fresh — overcounts.

signals — weights + decay. Δt = now latest fact ts for that key. no fact → drops out of the product (not a zero).

signal source fresh weight τ (min) rationale
desk active infer:hyprland (not-idle) 0.90 8 input = human at keyboard. strongest. τ forgives read/think gaps
page heartbeat infer:heartbeat 0.60 4 a surface you use is open + alive. pings ~30s; 4min gap = gone
wg handshake infer:wg 0.40 20 device on tunnel. coarse — pocket or three rooms away. weakest, slowest

threshold + hysteresis (schmitt trigger):

ENTER (away → present):  P ≥ 0.55
EXIT  (present → away):   P <  0.30
cold start:               away          # fail-closed; same as since(key)==null → don't fire

wide band = stable. a lone fresh wg (0.40) can't enter present but holds it while decaying — phone-on-network alone never declares you here.

scenario P bucket
at desk, typing ~0.90 present
at desk, 5min reading (client open) ~0.79 present
desk-only, ~9min zero input <0.30 → away
couch, phone page open, no desk 0.60 present
left house, only wg lingering decays ~20min → away
cold boot, nothing 0 away

implementation (core — presence reads State under the lock, predicate input, not a module):

data class Signal(val key: String, val weight: Double, val tauMin: Double)

val SIGNALS = listOf(
  Signal("desk_active",    0.90,  8.0),
  Signal("page_heartbeat", 0.60,  4.0),
  Signal("wg_handshake",   0.40, 20.0),
)

fun presenceScore(now: Instant, state: State): Double {
  var pAway = 1.0
  for (s in SIGNALS) {
    val last = state.lastTs(s.key) ?: continue          // no data → drops out
    val dt = Duration.between(last, now).toMillis() / 60000.0
    pAway *= (1.0 - s.weight * exp(-dt / s.tauMin))      // noisy-OR
  }
  return 1.0 - pAway
}

fun resolve(score: Double, last: Bucket) = when (last) {
  PRESENT -> if (score <  0.30) AWAY    else PRESENT
  AWAY    -> if (score >= 0.55) PRESENT else AWAY
}

each tick: score = presenceScore(now, state)bucket = resolve(score, state.lastBucket) → persist presence_state. decay uses wall-clock Δt, so 60s tick jitter causes no drift.

boundaries: hand-tuned, NOT feedback-tuned — keep presence numbers out of the auto-tuner or a weird week drifts you silently invisible. presence = reachability, not wakefulness — sleep/quiet-hours handled separately in the gate. feeds delivery routing directly. caveat (stated, not litigated): noisy-OR assumes independence; desk+heartbeat correlate (pc client open ⇒ both fire) — co-firing slightly overcounts, which is fine, genuinely co-firing IS stronger evidence.


proactive

listening — three modes, not one

"ambient listening" was the wrong frame — three capabilities split by trigger + retention. threat model is local-only: the file at rest (sqlcipher, extend to any retained audio/transcript) and who can reach the box (auth). that's it.

  1. address-capture — "maven, note this" out loud. hands-free voice path, no tap. raw audio dies after deriving the fact. ← mvp pick. lowest retention, highest daily payoff, ~free (voice path minus the tap)
  2. meeting-record — deliberate start/stop, verbatim transcript kept. retention is the point (overrides any "always delete" rule). post-mvp
  3. ambient-derive — background overhearing, low-confidence candidate facts, raw ephemeral, only derived fact survives. needs the confidence model working first. last

only mode 3 is always-on. modes 12 fire heavy transcription on explicit trigger/record → ryzen idles. always-on = lightweight VAD (+ maybe owner-detect) only. speaker-scoping = attribution metadata, not a kill-gate — tag source=ambient:self | ambient:other. per-person retention becomes a WHERE clause — "delete what you have from gf" is a query, not a problem.

delivery / channel routing

routing = f(severity, presence). presence decides reachability; severity decides insistence. need both.

present away
sev12 (care) voice drop
sev3 (ops, soft) voice ntfy, once
sev4 (ops, hard) voice + ntfy telegram, repeat til ack

sev ≤ 2 drops on away, sev ≥ 3 holds. a missed water nudge is noise; a missed backup-failure isn't. away-channels (ntfy/telegram) leave the box — the one path that crosses "never phones home," through your relay. minimal body — "disk low on homesrv," not detail. don't make notifications a shoulder-surf exfil surface.

feedback loop (outcomes → tune cooldowns)

the nudges.outcome column IS the signal — no new storage. cooldown becomes a function of recent outcomes, not a constant.

  • mostly ignored → nagging into the void → lengthen cooldown / raise threshold
  • mostly acted → landing → leave, or cautiously shorten
  • mostly snoozed → right nudge wrong time → shift the window, not the frequency

tunes parameters, never logic. can widen cooldown, nudge threshold, shift window. cannot rewrite a predicate or invent a rule. bounded knobs (cooldown ∈ [min,max]) so a weird week can't mutate maven silent or stalker. dead simple at mvp: a ratio over last N, not a learned model. ignored_rate > 0.7 → cooldown *= 1.5, capped. introspectable. persist the adjusted cooldown as a fact (source=feedback) — survives restart, stays visible; why maven went quiet should be a query, not a mystery.


auth

a cascade, not a pick-one

same shape as confirmation is not one mechanism — each layer answers a different question.

layer question mechanism surface
0 — tunnel on the network at all? wireguard everything. floor
1 — device enrolled box? mTLS client cert, terminated at proxy pc client, authed page
2 — session you, this session? passkey / webauthn pc client, authed page
3 — step-up you, right now, for this act? passkey user-verification gesture registration-enable, destructive acts, core cold-start

wg is necessary-not-sufficient: an unlocked laptop inside the tunnel is "authed" at layer 0 only — that gap is why the upper layers exist. passkey over password/token because step-up is load-bearing: webauthn gives per-assertion user-verification for free — the biometric/PIN gesture IS the human-in-the-loop. no shared secret on the box to steal; private key stays in enclave/TPM. spring security 6.4+ has native passkey support → in-stack for kotlin/spring. mTLS (layer 1) optional — if dropping one, drop mTLS, never the passkey.

the invariant — surface caps authority

auth tier is a property of the surface; the surface caps maximum authority. you can't step up past what the channel structurally carries.

  • voice presents layer 0 + speaker-attribution and STOPS. speaker verification is attribution, not auth. a room mic is reachable by gf, the TV, anyone present → voice is structurally incapable of layer 3
  • telegram inbound = possession of a telegram account + chat-id allowlist. telegram's auth, outside our control. weak tier → read + soft acts, never destructive, never registration

so voice/chat can never reach registration-enable — not because auth "failed" but because the channel can't carry the proof.

key-provenance

the theorem: no unattended key source survives a powered-on stolen box. anything the daemon fetches with no human present, a thief who grabs the running laptop fetches too. so the question was never "find the secure source" — it's pick the failure mode: unattended-but-loses-to-running-theft, or theft-resistant-but-attended. structural, can't have both.

the trap — TPM-alone: TPM-sealing binds release to PCRs, defeats offline disk extraction (real, worth having). but against full-box theft it does nothing — PCRs still match, thief boots, TPM unseals on cue. a laptop is portable → full-box theft is the likely case. TPM-alone protects the scenario you're less exposed to.

the resolution — cold-start IS a layer-3 act: unlocking the db makes maven's entire memory readable — the single highest-authority op. so daemon cold-start unlock IS layer-3 step-up. "always-on" = doesn't need babysitting during normal operation; does NOT mean survives a cold boot with nobody around. unlock is remote-attendedsystemd-ask-password over ssh, or pushed through the passkey-authed page. theft = box reboots into a locked daemon and stays there.

concrete stack:

layer mechanism buys
disk LUKS2, systemd-cryptenroll TPM2 + PIN offline-extraction dead (TPM), powered-theft needs PIN-in-head
sqlcipher key not at rest. supplied at daemon start via systemd credentials / ask-password, sourced from the authed surface, held in process memory only unlock is a deliberate gesture, never a file to steal

TPM+PIN is the honest middle. yubikey challenge-response is the upgrade path if carried on your body — don't reach for it yet. runtime: key in daemon RAM → mlock the page (no swap-to-disk), swap off or encrypted. zero on shutdown. the trade: a cold reboot needs you (remotely) present; in exchange a stolen laptop — running or off — is a brick holding ciphertext. matches the fail-closed posture.

core/module key-isolation

the convenience (reboot-attendance collapses to core cold-start only) is contingent on one thing: the key lives in core's address space and nowhere else.

  • core = the only key-holder. daemon holding sqlcipher-unlocked db + trigger loop. unlocked once (remote-attended), runs weeks. reboots almost never because it was deliberately given nothing that churns — no tool code, no model weights, no router. just state + loop
  • modules = restart-free, key-free, fail-independent. stt/tts, router/classifier, tool executors, delivery. update/crash/swap one → none touch the unlock. "update the tool module, no attendance" is correct by construction — the module never had the key

enforce it: IPC boundary, not shared address space (unix domain socket, local-only — a crashing tts can't read the key page). core mediates, never hands back a db handle — modules send requests to core ("write this fact" / "read presence"); core never returns raw db access. module compromise ≤ module authority — worst a popped tts does is send garbage audio.

the discipline: nothing enters core's process unless it must read state under the lock. loop qualifies. predicates qualify. phrasing, routing, transcription, tool execution, delivery all read derived data or send requests — none need the raw key. erode this and you buy back the attendance you just eliminated. systemd topology: core = one unit; each module its own unit, After=core.socket, socket-activated, Restart=on-failure.

the through-line

network → box → process: same question at three radii.

  • network (wg): who reaches the box
  • box (LUKS+TPM+PIN, sqlcipher): what a dead/stolen box gives up
  • process (core/module socket): what a compromised module reaches

every cut is the same instinct — a boundary you can move from inside isn't one, compromised X can't forge Y, attribution is not auth. auth didn't add a new principle; it applied the existing one at smaller and smaller scope.


open questions

four items the old decisions doc listed as open — router + invocation, two-memory routing, presence (concretely), auth — are now resolved by their respective sections above and are NOT listed here.

impactful — gate behaviour or the mvp surface

  • stage-3 confidence threshold — the gate-or-clarify number for the router. unset. defines how often maven asks vs. guesses on free-form input; the whole reactive mvp feel rides on it
  • quiet-hours definition — fixed clock vs derived from sleep facts. gates all proactive delivery; can't ship the loop's restraint without it
  • confirmation tier model — only the forced pin is settled (registration is out-of-band, human-only). the rest is a sketch: confirmation scales with specification + reversibility (rough buckets: just-do / binary-confirm / clarify-then-confirm / out-of-band). don't anchor on the count or any f(). open sub-item: destructive-act confirm — policy-level vs per-function flag
  • ask-password transportsystemd-ask-password over ssh vs passkey-authed page push for cold-start unlock. both work; unpicked. blocks remote-attended boot being real

plumbing / deferred — won't block the core build

  • passkey enrollment bootstrap — first credential on a fresh device before a passkey exists to authenticate with. the trust-on-first-use gap
  • mTLS in or out — provisioning cost on mobile vs paranoia payoff. leaning optional
  • PIN vs yubikey for LUKS — PIN-in-head pinned for now; yubikey deferred unless carry-on-body becomes the model
  • session lifetime / re-auth cadence — how long a layer-2 session lives before forcing re-assertion. unset
  • core↔module wire format — socket decided, the protocol on it isn't (length-prefixed json vs something tighter)
  • compound captures — "slept 6h, fan noise wrecked it" = one fact + one note in one utterance. classifier picks one label/utterance → compound splits (second pass) or loses half
  • query read-path — "when do i like backups?" is RAG-over-chroma, not the sqlite read the router table implies. query-answering is chroma-RAG OR sqlite-read depending on the ask
  • obsidian↔chroma mechanics — canonical md, derived embedding index, chunking. implementation of obsidian → chroma, unbuilt
  • presence — away tap override — explicit away tap as hard override (voids presence to away for a duration). clean extension, deferred; scoring stands without it
  • presence — weights/τ hand-tuning — first-guess numbers; expect tuning once the loop runs against real signal traces
  • presence — wg home-vs-cellular — distinguish home-wifi-via-wg vs cellular-via-wg to let wg carry more weight when clearly home. needs the signal to exist first
  • personality prompt — the identity character spec. unwritten
  • custom tts voice — train on a hand-picked voice — future: replace the piper ru floor (irina) with a voice kami picks himself, trained/fine-tuned. per the stt/tts note, training effort is where the personality budget goes — this is that budget. piper supports voice training; xtts/silero fine-tune too. unscoped: pick the voice + the trainer, dataset size, cpu-vs-gpu train. deferred until the reactive+proactive core is solid; the irina floor ships first.
  • daemon runtime — deferred until the homelab tool-call surface is real
  • listening modes 23 — meeting-record + ambient-derive. post-mvp; ambient-derive needs the confidence model working first