allMemVectorMetas (memory.go) replaces the identical query-then-scan
block ReembedAll and RepairFactVectors each had for reading id+meta
out of memory_vectors — same query, same json.Unmarshal, different
structs built from the result.
Two RowsAffected() errors were silently dropped with `_`, inconsistent
with every other call site in the same files: AcceptProposedRoutine
now wraps the error instead of treating it as zero rows, and MarkAcked
had it stranded behind a dead branch (both arms returned nil) removed
along with the swallowed error.
No behavior change; internal/store and internal/memory pass with
-race.
Two review threads from PR 4, and the answer to the third.
The routine status was a bare string with its legal set in a comment.
Nothing caught a typo at compile time, nothing enumerated the set for a
test, and a bad value surfaced as a /routines row that neither accepts nor
dismisses. It is a RoutineStatus now, with the three constants, a
RoutineStatuses slice as the single source of truth, and Valid(). Listing
by an unknown status is refused with ErrRoutineStatus instead of answering
"no rows", which is what a correct query says about an empty table. A
round-trip test moves a routine into each state and reads it back, so a
constant that drifts from the inline SQL fails loudly.
The hand-rolled framing stays, and frame.go now says why: ninety lines,
readable with socat, and every standard replacement brings schema
machinery this boundary does not want. What was wrong was inheriting it
untested. frame_test.go covers the paths a real socket produces and the
round-trip test never does — truncated header, truncated body, one byte
per Read, two frames back to back, and a non-JSON body. Empty input is the
only EOF.
The unanswered question in the same file is answered in place: a routine
object stays a local string, not a Nexus ref, because nothing acts on it.
It is the word he used, replayed back to him, compared only against itself
for the UNIQUE key. Canonical refs arrive if a routine ever drives a Hexis
call, which is V-272.
The mood enum has the same shape and is not done here: it is spelled in
the GBNF grammar, three prompts and the parse, so it is its own change.
detectPattern only ever fired as a side effect of a voice fact-write, so a
recurring pattern already sitting in history went unnoticed until he
happened to mention it again by voice — the opposite of proactive.
Split the pipeline: extraction (fact -> normalized event) stays where a fact
is written, in voice.go, since it's tied to that write regardless of who's
talking. Detection (events -> stable pattern -> proposed_routines row) moves
into shared code (patterns.go's detectAndPropose) that both the voice path
and the new tick.go:detectPatterns call. The tick runs it every cycle over
every action+object pair on record (store.DistinctEventPairs, added), so a
pattern gets noticed on the daemon's own schedule.
Idempotence and the dismiss-must-stick requirement turned out to already be
handled by the store, not something the tick needs to reinvent:
proposed_routines has UNIQUE(action, object) and CreateProposedRoutine does
ON CONFLICT DO NOTHING, and DismissProposedRoutine flips status in place
without deleting the row. So a pair already proposed, accepted, OR
dismissed is a silent no-op on every later tick — a dismissed pattern can
never resurface, and re-running the scan never spams the /routines page.
Kept the voice-path call (immediate spoken confirmation is a nice feature
UX-wise and is now redundant-but-harmless with the tick, since both paths
share the same guarded detectAndPropose).
Tick-side detection only ever writes a row; it does not notify, ring, or
speak, keeping Maven "not a nag, not autonomous" — the /routines page is
still the only place a proposal becomes visible, and only accepting it
starts producing nudges (fireAcceptedRoutines).
Also fixed the stale vikunja#46 reference in proposed_routines.go — the
TODO it named is what this commit does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
Two merge fixes on top of the branch:
- migrations: keep both new steps, snooze stays #8, the routine columns
become #9. Both agents had numbered theirs #8.
- accepting no longer takes a reminder id, on the web surface too. The
web accept path had the same one-shot-reminder bug the voice path did,
so both now just flip the status and let the tick loop schedule.
The test that asserted "accept creates a reminder and links it" asserted
the bug. It now asserts that accepting creates no reminder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
Data layer only. Migration #8 adds accepted_ts and last_fired_ts to
proposed_routines, plus ListAcceptedRoutines and MarkRoutineFired so the
tick loop can own the schedule. Accepting no longer links a reminder id.
Look at the TODO(vikunja#366) in cmd/mavend/tick.go for the next commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
Look at internal/store/proposed_routines.go: status flips in place with an
`AND status = 'proposed'` guard, not append-only like facts/voids_id — a
proposal is a question with one answer, same shape as tools.status. The
UNIQUE(action, object) key is what stops a dismissed routine coming back.
New tests cover re-propose-after-dismiss and listing by status.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ