Skip to content

Feat/memory - #17

Merged
armedev merged 12 commits into
mainfrom
feat/memory
Aug 25, 2026
Merged

armedev merged 12 commits into
mainfrom
feat/memory

Conversation

@armedev

@armedev armedev commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

No description provided.

armedev added 12 commits August 25, 2026 16:17
New pure dates.ResolveRelative resolves "today/tomorrow/yesterday,
in N days|weeks|months, next week|month, [next] <weekday>" against the
capture clock - no extra LLM call, absolute date strings deliberately
pass through unresolved.

Resolved forms land in two places: frontmatter renders
'- tomorrow -> 2026-08-27' via an index-aligned DateResolutions slice on
the entities block, and the entities table gains a nullable resolved_date
column (idempotent ALTER) so follow-up queries can filter on real dates.
Assembles a per-capture context block and injects it into the four
enrichment prompts so tags/summaries/entities are consistent with who
the user is and what khayal already knows.

Block composition, in order: config user_context (own 2000-char cap),
people/orgs config blurbs matched case-insensitively against the capture
text, LLM-maintained memory file contents, entity glossary (most frequent
persons+orgs), and top-3 semantically similar past note chunks via
TopSimilarChunks. Derived content shares a 1200-char cap; empty sources
yield no injection and clients without call-context support are
unaffected.

memory.enabled=false short-circuits before any embed or retrieval.
Enrichment now runs four LLM calls per capture (entities added in phase 1
runs sequentially after the parallel three).
…se 2.5c)

<inbox>/memory.md (filename configurable via memory.file) is khayal's
long-term memory: seeded as a headed skeleton on startup (# Memory /
About the author / People / Ongoing threads / Preferences), consolidated
by the LLM in a merge-style rewrite, and injected back into enrichment
as part of the context block.

- new chained 'memory' job type, gated by a throttle: runs only when
  >24h since the last consolidation or >=5 new distinct persons
  accumulated (markers in stats_cache)
- consolidation prompt preserves unrelated content and manual edits,
  keeps headings stable; output sanitized + atomic write behind mtime
  guard; previous file survives until a rewrite succeeds
- managed-file exclusions: reindex scan and connection wikilink targets
  never touch memory.md; it is never indexed or retrieved as a note
- queue gains GetStat/SetStat/CountPersonsSince; vault gains
  WriteManagedFile/ManagedFileExists/ReadManagedFile

Verified live: skeleton seeded on start; one capture chained
text -> connections -> memory; John Doe/Acme facts merged into People
and Ongoing threads sections.
consolidation_interval_hours and new_persons_threshold move from hard
constants into the memory config section. Both are *int with nil/negative
falling back to the previous defaults (24h / 5); explicit 0 is honored -
interval 0 means every capture can trigger a consolidation, threshold 0
means any single new person does.

Docs updated: config.example.yaml documents both knobs and clarifies the
mechanism (background job queue chaining, never a cron); SPEC.md gains
the static-user-memory + memory-file bullet points; VAULT.md notes the
throttle fields.
The consolidation model sometimes echoed the user-prompt section labels
(RECENT CAPTURED FACTS: / CURRENT MEMORY FILE: / NEW PEOPLE SINCE LAST
RUN:) into its output. Because consolidation is merge-don't-remove, that
pollution then compounded every cycle.

memory.SanitizeConsolidatedOutput now truncates everything from the first
echoed label, requires output to start with '# Memory' and contain all
five canonical headings, and cleans dangling bullet dashes. Broken output
returns an error so the job retries instead of persisting garbage; the
previous file survives untouched until a rewrite succeeds.

Consolidation prompt hardened with matching explicit rules. Verified
live: previously polluted memory.md rewritten clean (0 fragment leaks)
with new facts still merged.
chainMemoryConsolidation linked its job id onto the ingest job,
overwriting the connections pointer that chainConnections had just set -
clients polling an ingest job were silently redirected to the memory job
(whose result is empty), hiding live connection results.

The implicit call inside chainConnections is also decoupled: processJob
now invokes both chain functions explicitly, so each can be reasoned
about (and tested) independently.

Comprehensive worker regression suite added covering: pointer set +
target validation, disabled/empty-path no-ops, the clobber regression,
throttle gates (disabled memory, fresh marker suppression, new-persons
threshold trigger), and managed-file exclusion from connection links.

testdata rows repointed to their real connections jobs; verified live:
fresh capture's pointer resolves to a connections job with populated
results.
…se 2.5 wrap-up)

Live captures showed small-model flakiness: names filed under tags but
missing from people, leaving enrichment blind. Three layers of fix:

- Glossary rescue pass: when extraction returns ZERO people, known
  glossary names appearing in the capture text are promoted deterministi-
  cally (no extra LLM call). Non-empty extractions are never overridden.
- Root-cause bug fixed en route: GetEntityGlossary scanned one column
  from a two-column grouped query and silently swallowed the per-row
  errors - the glossary has been empty since it was written.
- Default system prompts redesigned from observed failure modes:
  ExtractEntities insists names are extracted even when used as topics,
  bans years-as-amounts, requires consistent casing per entity; Consoli-
  dateMemory gains anti-drift rules (collapse repeated meetings into one
  counted line, no cross-person fact copying, drop rhetorical noise,
  opinions only in Preferences).

Verified live: 'what is bobs issue...' - previously produced zero
people/connections - now extracts Bob and surfaces three person
connections.
…ned memory rewrites

The 3b text model kept violating consolidation merge discipline:
duplicated sections, echoed prompt labels as headings, preserved
rhetorical questions. Model upgrades beat more prompt nudging.

- config: optional llm.consolidation_model; empty or equal to
  text_model reuses the primary client via NewConsolidationLLM
- worker: SetMemoryLLM installs the dedicated client; processMemory
  calls GenerateWithSystemTemp at 0.2 when available - consolidation
  is a deterministic merge, not a creative task
- sanitizer: collapse canonical headings that reappear later along
  with everything under them (the repeat is always the weaker copy);
  table-tested against real observed garbage output
- timeouts: generation/vision now use OllamaGenerateTimeout/Vision-
  Timeout constants bumped to 300s - cold-loading a 7b model blew
  the hard-coded 120s and cost a retry
- prompts: each heading exactly once, never write "None" filler

Verified live on qwen2.5:7b: single People section, meetings
collapsed with counts, repayment fact recorded accurately (owes 50,
not 100), zero structural artifacts across two clean cycles.
…escue

Documents the phase 2.5 wrap-up work that shipped in fb175ca/e536128:
llm.consolidation_model config, temp-0.2 consolidation, structural
sanitizer rules, and the zero-people glossary rescue pass.
Google-Gemini pattern: an explicit CTA synthesizes a grounded answer
from the search results the user already has. Never automatic, no
config option - the param/button IS the switch.

API (GET /v1/search?overview=true):
- Search runs unchanged first; overview is one RAG call over the top
  min(5, len) result excerpts at temperature 0.3 via optional-interface
- Fail-open: any generation failure returns overview:null with full
  results and a warn log; zero results or missing param = zero LLM calls
- extractCitations keeps unique in-range [n] refs in first-appearance
  order; out-of-range refs are dropped, never clamped into wrong notes
- Mock-LLM handler tests cover all four contract cases

kl:
- kl search --answer fetches with overview and renders a themed panel
  above the results with muted source lines; null -> 'AI answer
  unavailable'; 180s request timeout for the LLM round-trip

PWA:
- Gold-accented CTA pill beside the filter chips, rendered only once
  results exist; clicking is the only trigger - nothing watches state
- Skeleton thinking-state reuses the shimmer loader: pulsing badge +
  four staggered bars where the answer lands
- Answer card: gradient-border panel, [n] tokens as gold superscript
  chips that smooth-scroll to anchored result cards, regenerate/dismiss
  actions; error state offers retry without touching results

Verified live against testdata vault with qwen2.5:7b: 10 results,
answer citing [2] and [4], no-param requests carry no overview field.
Docs catch-up for consolidation routing + glossary rescue included.
Replaces the header pill + floating box with a first-class row at the
top of the results list. Collapsed it looks like part of the list
(sparkles + 'AI Answer' + chevron); clicking expands it fluently in
place - height animation, shimmer skeleton, then the answer.

Expansion state is decoupled from fetch state: collapse keeps the
answer cached, re-expanding is instant and free; dismiss resets fully.
Header click semantics: idle -> ask+expand, ready -> toggle.

Component tests rewritten for the six interaction states; assets
rebuilt; live-verified end to end.
SPEC: inline expanding row wording, temp 0.3, top-5 excerpts,
citation clamping, zero-LLM-call contract. REPO_STRUCTURE: overview.go,
AIAnswer.tsx, useAIAnswer.ts. UI_SPEC: AI answer row interaction spec.
ARCHITECTURE: search flow gains the optional overview leg.
@armedev
armedev merged commit a723525 into main Aug 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant