vault

package
v0.5.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 22, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package vault reads and writes notes in the Obsidian vault.

The markdown file is the source of truth. Reading is forgiving — anything it cannot parse is preserved by simply not touching it. Writing is rare and deliberate: see WriteAtomic, the single door through which anything enters the vault.

Index

Constants

View Source
const (
	// FileMode is owner read/write, nothing for anyone else. Every file this
	// product creates inside the vault.
	FileMode os.FileMode = 0o600
	// DirMode is owner-only traversal. A directory that is group- or
	// world-executable lets anyone walk in and stat what is inside, which for a
	// vault leaks the project names and session times even when every file in
	// it is unreadable.
	DirMode os.FileMode = 0o700
)

The vault holds the user's private memory: what they told an assistant, who they work with, and every prompt they typed. It lives in their home directory, which on a shared machine is readable by everyone with an account unless something says otherwise.

Nothing did. Notes came out 0600 only because os.CreateTemp happens to make files that way and WriteAtomic renames one into place — while the two files that hold *everything*, the activity log and index.db, were written 0644 by their own callers. So the careful-looking permission on the markdown was undone by a world-readable database containing the same text.

These constants make the intent explicit rather than incidental, so a new writer inherits it instead of rediscovering it.

Variables

This section is empty.

Functions

func Chosen

func Chosen() (dir string, explicit bool)

Chosen reports the vault path and whether anybody actually chose it — set LOGOS_VAULT in this process, or recorded a path with `logos setup`. When neither is true the answer is the ~/logos default, which nobody has vouched for, and a caller may treat "it is not there" as "make it" rather than as a mistake to report.

func EnsureDir

func EnsureDir(path string) error

EnsureDir creates a file's parent directory privately.

func EnsureGitignore

func EnsureGitignore(vaultDir string) (wrote bool, err error)

EnsureGitignore makes sure a vault that lives inside a git repository never offers .logos/ — the SQLite cache index.db rebuilds from markdown on every `logos index` — or the activity log to be committed. It reports whether it wrote anything, so a caller can announce the change rather than let it happen silently.

Two clones of the same vault each keep their own cache: it is a local index, not shared state, and a binary file with no information the markdown doesn't already carry is exactly the kind of thing that turns every `git pull` between two people sharing a vault into a merge conflict for no reason. This is the fix from the "vault two people can share over git" plan: markdown is truth, and the cache never touches git at all.

It only ever appends. A .gitignore a user wrote for their own reasons keeps every line they put there, and a second run that finds a rule already covered — by this exact line or one they wrote themselves — changes nothing, so `logos index` calling this every time never turns into a diff on a file nobody meant to edit.

func MkdirPrivate

func MkdirPrivate(dir string) error

MkdirPrivate creates a directory the way the vault wants it.

os.MkdirAll does not change the mode of a directory that already exists, so this does not silently retighten a vault the user set up deliberately — new directories are private, and existing ones are left as they are and reported by `logos doctor` instead. Tightening someone's filesystem without asking is its own kind of surprise.

func NormalizeLink(target string) string

NormalizeLink reduces a wikilink target to its trailing segment. Full entity resolution lands in step 3; this is the cheap normalisation that makes [[Sameer Rao]] and [people/sameer-rao] agree.

func Path

func Path() string

Path resolves where the vault lives. One answer, for every front end.

The order is explicit override, then recorded choice, then default:

  1. LOGOS_VAULT, which is how an MCP host config pins a server to one vault and how a scratch vault is used in a test or a shell. An explicit instruction in the current process wins over anything on disk.
  2. The path `logos setup` recorded. This is what makes the desktop app find a non-default vault, since it has no environment to inherit.
  3. ~/logos. Absolute, because a relative default is resolved against whatever directory a host happened to launch the binary from — which is the original version of this bug.

func Pointer

func Pointer() string

Pointer returns the vault path written by Record, or "" if there is none — whether or not that directory is there right now.

It used to be ignored when the directory was missing, falling back to ~/logos. A missing recorded vault is most often on a drive that is not mounted, and the fallback split one project across two vaults: the MCP server created ~/logos and accepted checkpoints into it, and after the drive came back `resume` found none of them. So the recorded path stays the answer, and every caller that opens a vault checks it exists first and refuses by name. Someone who really did delete it moves the pointer with `logos setup --vault`.

func Previous added in v0.4.5

func Previous() string

Previous returns the vault path the pointer held before the most recent Record, or "" if this machine has only ever had one.

func Private

func Private(path string) error

Private sets FileMode on a path that already exists, ignoring absence.

For files this package does not create itself — index.db, which the SQLite driver opens with its own mode. Ignoring a missing file matters because the caller uses this on paths the driver creates lazily.

func PrivateSiblings

func PrivateSiblings(path string) error

PrivateSiblings applies Private to a path and the sidecars a SQLite database keeps beside it. The -wal file is the one that matters most: it holds pages not yet checkpointed back, which is to say the most recent writes.

func Record

func Record(dir string) error

Record writes down which vault this machine uses, so a front end with no environment to inherit can still find it. Called by `logos setup` once the vault is known.

Failure is returned rather than swallowed, but callers treat it as a warning: a machine where the config directory cannot be written still has a working vault, it just has one the desktop app can only find if it is at the default.

func Recorded

func Recorded() string

Recorded is Pointer, but only when the directory exists. For callers that are describing the recorded vault rather than opening it.

func Slug

func Slug(vaultDir, path string) string

Slug turns vault/people/Sameer Rao.md into people/sameer-rao.

func WriteAtomic

func WriteAtomic(path string, data []byte) error

WriteAtomic writes via a temp file in the same directory then renames, so a crash mid-write cannot leave a truncated note and Obsidian never observes a partial file.

Every path that puts a file into the vault goes through here — the rollup applying an accepted proposal, and an agent committing a checkpoint. One writer means one set of guarantees, rather than each caller reinventing how carefully it wants to be interrupted.

Types

type Edge

type Edge struct {
	Pred string  // predicate, e.g. works_on; body wikilinks use "mentions"
	Obj  string  // target note slug, [[ ]] stripped
	Conf float64 // 0..1
	Src  EdgeSrc
}

type EdgeSrc

type EdgeSrc string

EdgeSrc records where an edge came from, which determines how much we trust it.

const (
	// Stated: you wrote it — a [[wikilink]] or an explicit frontmatter relation.
	Stated EdgeSrc = "stated"
	// Inferred: a model proposed it and you accepted it.
	Inferred EdgeSrc = "inferred"
	// Imported: pulled in from an external system.
	Imported EdgeSrc = "imported"
)

type Guard

type Guard struct {
	// contains filtered or unexported fields
}

Guard is a held lock. Unlock is safe to call exactly once, and callers defer it immediately.

func Lock

func Lock(vaultDir, name string) (*Guard, error)

Lock takes the vault-wide lock named name, blocking until it is free.

name is a bare identifier — "memory-fact", "memory-pending" — so that writers to different files do not queue behind one another. Callers that need two locks must take them in a fixed order; today the only nesting is a kind lock outside the pending lock.

A vault on a filesystem with no working flock (some network mounts) is not a reason to refuse to save a memory, so the OS half degrades to the in-process half and says so on stderr rather than failing the write. It is announced because a silently weaker guarantee is how this class of bug got here.

func (*Guard) Unlock

func (g *Guard) Unlock()

Unlock releases both halves, in the reverse order they were taken.

type Note

type Note struct {
	Slug    string
	Path    string
	Title   string
	Kind    string
	Aliases []string
	Body    string
	Edges   []Edge
	Hash    string // content hash, so reindexing only re-embeds what changed
	// FirstSeen is a unix timestamp parsed from frontmatter, 0 if absent. It is
	// what the graph's time scrubber animates over: when each note entered the
	// vault.
	FirstSeen int64
}

func Parse

func Parse(vaultDir, path, raw string) Note

Parse reads one note. Malformed frontmatter degrades to "no metadata" rather than dropping the note: hand-edited vaults will contain broken YAML, and silently losing notes is the worst available failure.

func (Note) EmbedText

func (n Note) EmbedText() string

EmbedText is what actually gets embedded. Title and kind are prepended so a query like "who is sameer" can match an otherwise sparse stub note.

type Stamps added in v0.5.0

type Stamps struct {
	// contains filtered or unexported fields
}

Stamps remembers the hash of a whole-file record as this process last wrote it, so a store about to rewrite that file can tell "nobody has touched this since we wrote it" from "the user edited it by hand".

The stores that rewrite a whole file from the cache — loops.md, the dream insight queue, the memory review queue — each told the user that deleting a line discards the record "on the next `logos index`". Any write before that index regenerated the file from the cache and put the deleted line straight back, so the edit only held if the user happened to reindex first. Adopting the file before every rewrite fixes that, and this is what makes adopting cheap: the common case, where the only writer was us, is one file read and a hash compare instead of a full pass.

A hash rather than size and mtime, for the reason internal/memory gives: the failure a cheaper check admits is silently losing the user's edit, which is the bug this exists to prevent.

Process-local. A stamp we do not have means a full pass, so another process writing the file costs one reconcile, never a missed edit.

func (*Stamps) Adopted added in v0.5.0

func (s *Stamps) Adopted(key any, raw []byte)

Adopted stamps bytes the caller has just read and adopted, for a reconcile that is not followed by a write.

func (*Stamps) Forget added in v0.5.0

func (s *Stamps) Forget(key any)

Forget drops the stamp for key, so the next reconcile does a full pass. A store calls it when it is rebound to another vault: the stamp described a file in the old one.

func (*Stamps) Ours added in v0.5.0

func (s *Stamps) Ours(key any, raw []byte) bool

Ours reports whether raw is exactly what this process last recorded for key.

func (*Stamps) Record added in v0.5.0

func (s *Stamps) Record(key any, path string)

Record stamps the file at path as ours. It reads the file back rather than hashing what the caller meant to write, so the stamp describes the disk.

A read that fails clears the stamp instead of leaving the old one. The usual cause is not an error at all — a store whose last record went away removes its file — and a stale stamp is a claim that bytes on disk are ours: restore that file from a backup, the claim matches, the reconcile is skipped, and the next write overwrites the restore.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL