Skip to content

Feat/proactive connections - #16

Merged
armedev merged 7 commits into
mainfrom
feat/proactiveConnections
Aug 25, 2026
Merged

armedev merged 7 commits into
mainfrom
feat/proactiveConnections

Conversation

@armedev

@armedev armedev commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

No description provided.

After a successful ingest, the worker chains a connections job for the
note. Polling the original job now reveals connections_job_id; polling
that job returns ranked connections stored in a new jobs.result column.

Three detectors, all gated by config and never surfacing notes younger
than min_age_days (7 default):

- similar: reuses the note's own stored chunk embedding via SearchSemantic
  with a date-cutoff filter - no extra LLM call
- person / amount: entity-table lookups with SPEC-format labels
  ("Alice also appears in N other notes")

Ranking dedupes per note keeping the highest-priority occurrence
(person > amount > similar), sorts, and caps at max_per_capture.
Detector errors skip their type instead of failing the job; chaining
failures are logged and non-fatal.

Deviations from the phase doc, forced by the real codebase: no
re-embedding step (chunks already exist), correct age-filter direction
(doc had it inverted vs SPEC), connections job created post-ingest rather
than at capture time since note_path does not exist yet.
initSchema ran the phase-2 ALTER statements through the strict
fail-on-error loop, so any database already carrying the new columns
failed to open on second server start. Column adds now run in their own
pass where duplicate-column errors mean already-migrated and are
treated as success; a regression test opens the same DB twice.
ListJobs and GetPendingJobs scanned note_path/source_url/source_file/
user_context/content/error into plain strings, so any row with a NULL
in those columns 500'd the whole queue view. Hand-seeded SQL fixtures
triggered it; nothing prevented production rows from doing the same.

Both scans now use sql.NullString per column (FetchAndLockPendingJobs
already did), covered by a regression test that inserts a row with
every optional column NULL.
After a connections job completes, ranked targets that exist on disk are
written into the note's frontmatter as a connections: block of quoted
wikilinks (- "[[note-name]]"), so Obsidian's graph and properties see
what khayal found. Body text is never touched.

SetConnections splices only the top-level connections key (replacing any
stale one), skips the write entirely when nothing changed, restores mtime
after writing so external-change detection elsewhere keeps working, and
the worker pre-verifies every target on disk - vault safety rule #5,
never write broken wikilinks. Verified live: capture mentioning an older
note's entities produces the block while the body survives byte-for-byte.
…, amount corroboration

Live results were dominated by coincidental number matches: any capture
mentioning $2,000 connected to every other note containing 2000,
regardless of context. Meanwhile genuinely related notes never surfaced
because the 0.85 threshold sits above most useful nomic-embed-text
similarities (measured: unrelated ~0.49, related ~0.79, identical ~0.90).

- TopSimilarChunks: raw-cosine connection search (no rescaling, self and
  age excluded) so Connection.Score is true confidence
- default similarity_threshold 0.85 -> 0.72, inside the measured gap
- amount corroboration guard: an amount match surfaces only when the pair
  also shares a person entity or scores within 0.10 of the similarity
  threshold; bare number equality is suppressed
- person detector unchanged; ranking/dedup unchanged

Verified live: John Doe/$2,000/Acme capture now yields empty connections
(was: spurious Alice-budget match); Alice captures still connect with
correct labels.
… explicit zero min-age

Three stacked issues surfaced when same-day Bob captures produced no
connections:

1. Person casing varied per LLM run ("Bob" vs "bob") and entity SQL
   compared exact strings, so cross-note person joins silently missed.
   Entity lookups now match case-insensitively via LOWER().

2. No guard against extractor junk: "I" was being stored as a person.
   Normalization now rejects single letters and a pronoun stoplist, and
   the extraction prompt forbids author/pronoun/single-letter people.

3. ApplyDefaults coerced an explicit connections.min_age_days: 0 back to
   the 7-day default (indistinguishable from unset), making same-day
   connection setups impossible to configure. MinAgeDays is now *int:
   nil/negative -> 7, explicit 0 honored.

testdata config sets min_age_days: 0 for same-day experimentation.
- REPO_STRUCTURE: real connections/ tree entry (drop six phantom files),
  remove nonexistent internal/search package, jobs table gains result +
  connections_job_id, entities table schema documented with the two
  independent writers, JobStore + Writer interface snippets refreshed
- ARCHITECTURE: capture flow shows the connections chaining step
- VAULT.md: frontmatter connections wikilink block contract
- phases/v1.1: phase 2 marked shipped
- openapi.yaml: Job schema gains result and connections_job_id
@armedev
armedev merged commit 0cf5e79 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