Commit Graph

12 Commits

Author SHA1 Message Date
kami b0f5a16ec9 Add digest as a real outcome: suppressed care nudges get resurfaced, not lost
Vikunja #281. The interruption policy promised four outcomes — deliver_now,
queue, digest, drop — but only three existed: a care candidate the restraint
gate suppressed for quiet hours / away / calendar-busy simply vanished in
loop.Tick's `continue`, with only the trace remembering why.

internal/morning turned out not to be the natural drain: it's a fixed
Item/FactKey checklist engine, not a generic message bundler, so gate-
suppressed nudge text has nowhere to plug into its evidence model. Built a
parallel (but small, reusing the outbox's shape) durable digest instead:

- internal/store: digest_entries table + EnqueueDigestEntry (dedupes by
  rule+body, mirroring the delivery outbox's bodyHash), PendingDigestEntries,
  ExpireStaleDigestEntries, DrainDigestEntries (mark, never delete — an
  audit trail of what she actually said).
- internal/loop: DigestEligible(severity, blockedBy) is the pure boundary —
  only genuine restraint blocks (quiet_hours/calendar_busy/presence) even
  qualify (cooldown/snooze are not "suppression"); within care, Sev2 (break)
  digests, Sev1 (water/meal — stale by the time anyone could resurface them)
  drops. High severity never digests; alarms bypass the gate and deliver
  unchanged, on purpose.
- cmd/mavend/tick.go: each tick scans ExplainTick's trace for eligible
  blocked candidates, enqueues them, sweeps stale entries (24h expiry — the
  care rules are daily-cadence, so anything older is describing a day
  that's over), and drains the bundle only once the suppression reason has
  actually cleared, capped at 3 spoken items plus a trailing count so a
  digest can't turn into the exact nagging it was built to avoid.

Tests: store-level round-trip/restart-survival/dedupe/expiry/drain, loop-
level severity-boundary unit tests, and tick-level integration tests for
the drain-only-when-clear and never-digest-high-severity behavior.
2026-07-31 23:09:22 +04:00
kami 1f55207b58 Merge the snooze read path and wiring
# Conflicts:
#	internal/loop/gate_test.go
2026-07-31 02:33:41 +04:00
kami 2f00593411 Wire the snooze read into the Gatherer and honour it for reminders (#364)
The Gatherer now fills State.SnoozeUntil from store.SnoozedUntil instead
of nil, so a snooze finally reaches the gate. RemindDecisions gains the
one restraint check that applies to a reminder — quiet hours, presence
and cooldown are still bypassed, so "wake me 7" is unchanged. Reviewer:
the two tests in internal/loop/gate_test.go are the contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:32:24 +04:00
kami 9d8fcf42f3 Test the universal restraint gate, including two gaps
Pins the conservative side of Gate(): quiet hours, away, calendar-busy,
cooldown and snooze, plus one nudge per tick at max severity. Reviewers: the
two skipped tests at the bottom are real gaps, not flakes. Reminders ignore
snooze (loop.go:120) and the Gatherer never fills SnoozeUntil (gather.go:153),
so snooze does nothing at runtime. No behaviour was changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:13:46 +04:00
kami 9e2af3286b Unit-test all five proactive rule predicates
Table-driven tests for water, meal, break, service_down and netdata_critical,
straight against the predicate with a fake State. Reviewers: the no-data rows
(every rule must stay quiet when its key is missing) and the ops forgery rows,
where a fact with the right value but the wrong source must be refused.
No rule fired on missing data, so no fix was needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:12:25 +04:00
kami 76a6a007ef Pin the resident model to Qwen3.5-0.8B and name Qwen3-1.7B as the target
The most load-bearing decision in the project was stated four incompatible
ways: the docs said Qwen3-1.7B, deploy/mavend.json said Qwen3.5-2B, the repo's
models/llm/ held an LFM2.5-1.2B gguf, and five code comments still said LFM.
Answering "which model is deployed" meant re-deriving it from scratch every
time.

Two facts the review missed, found while resolving it:

- /mnt/hdd1/llms is bind-mounted over /opt/maven/models/llm, which shadows the
  repo's models/llm/. The LFM2.5 gguf sitting there was never loaded by
  anything, so it was not evidence of the deployed model at all.
- That library holds Qwen3.5-0.8B, -2B and -4B, and no Qwen3-1.7B. The config
  pointed at a file that does exist; the docs' Qwen3-1.7B was the stale claim,
  the reverse of the assumed direction. Qwen3-1.7B is the CPT target, and that
  training is still in flight (Vikunja #122), so no such gguf exists yet.

phraser.model_path moves to Qwen3.5-0.8B (Q4_K_M) — the smallest checkpoint on
disk, chosen for latency, and relevant to whether the LLM router is affordable
on this box. Docs and comments now say the same thing in one voice: 0.8B
resident now, CPT'd Qwen3-1.7B as the target, and the bind-mount shadowing
written down so the next reader does not mistake models/llm/ for ground truth.
Comments name the model, never a filename, so a swap stays a one-line config
change.

n_gpu_layers: 99 is correct and stays — compose passes /dev/dri and the render
gid for Vulkan offload to the Vega iGPU. CLAUDE.md's "CPU-only" was the stale
half of that contradiction and is corrected.

phraser.go also dropped a wrong "sub-1b, prompted not trained" size claim: the
target is trained end-to-end (RU CPT + joint persona/router SFT).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-30 23:40:33 +04:00
kami d52f60c54e maven: fix test mocks for CalendarEvents interface (verification)
- Add CalendarEvents method to recordingAPI in auth_test.go
- Add CalendarEvents method to fakeCore in handlers_test.go

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:20:16 +04:00
kami 8d823000d1 fix: code-review findings on overnight-jul5
- digest queue no longer dropped on failed dispatch (retry next tick)
- collapsed reminders marked fired/rescheduled only after digest delivers
- /tools step-up gate skipped when WebAuthn is not configured (was 403 forever)
- passkey credential store rolls back memory on persist failure
- auth_test fake updated for TickTrace (branch build break)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASstMtsZWLSRcD1Tq8T68Q
2026-07-05 17:50:21 +04:00
kami 2689db1248 loop: add rule trace/explanation engine
ExplainTick and ExplainGate produce a full TickTrace for every tick,
recording per-rule: predicate result, gate result, first gate blocker,
gate detail (snooze/cooldown/presence etc.), and win/loss info.

IPC layer: new MethodTickTrace, DTOs (TickTrace, RuleTrace, GateDetail),
dispatch, client proxy, and CoreAPI interface method.

Daemon: tickLoop caches the latest trace; daemonAPI wraps storeAPI
with a TickTrace override that returns the cached trace.

Tests: 15 new tests for ExplainGate (all blockers, bypass conditions,
ordering) and ExplainTick (nothing fires, one fires, tiebreak,
winner/lost_to recording, gate-blocked recording).
2026-07-05 13:14:58 +04:00
kami ca081ce84d loop: collapse stale-reminder burst into single digest notification
When the daemon starts after being offline, multiple due reminders
would fire simultaneously as separate notifications. Now a single
digest reminder is dispatched instead, summarizing all pending items.

- New collapseReminders() helper in gather.go: if 2+ reminders are
  due, marks originals as fired and returns one synthetic reminder
  (ID=0) with a combined JSON payload.
- Dispatcher skips MarkReminder for ID=0 (synthetic digest).
- All loop and delivery tests pass.
2026-07-05 02:15:07 +04:00
kami 36233058dd quiet-hours: configurable time-window schedule
Adds QuietHours config section (start/end as HH:MM local time). The loop's
gatherer checks the window each tick: if now falls within [start, end), the
State.QuietHours flag is set to true regardless of the config fact (which
the voice toggle writes independently). Both sources activate quiet —
schedule AND toggle.

Handles midnight-crossing windows (23:00-08:00). The gate already reads
State.QuietHours for care nudge suppression — no gate change needed.
2026-07-03 13:21:08 +02:00
kami 612583d59a initial commit 2026-07-03 00:32:48 +02:00