How I built self-maintaining projects for my desktop AI companion: folder attach, one work-log line, and a session-scoped undo
The pain is mundane, which is why it kept biting. I work across several folders in a day — Ankita itself, a demo site, a scratch repo — and every assistant I tried either remembered nothing about the project I was in, or remembered everything globally, so notes from one project bled into another. I wanted a middle setting: open a folder, and the companion quietly knows which project it is, keeps a small record of what happened there, and lets me undo exactly what it wrote on its own. Nothing more.
Ankita's project system does that with three deliberately boring mechanisms. Boring is the point — each one is deterministic, so there is nothing to argue with except the design choice at the end.
The folder decides, not a global switch
A project is a root path plus notes, decisions, todos, and a work log, stored in src/memory/projects.mjs (ProjectStore). When a session starts, detectProject in src/memory/project-resolver.mjs resolves the working directory against every stored root. The rules are strict on purpose:
- A root is a root, never a loose substring.
C:\foomust not captureC:\foobar. Matching is boundary-safe via relative-path checks, and case-folded only on Windows, where the OS itself is case-insensitive. - The longest matching root wins, so a nested project inside a bigger one behaves predictably.
- If two roots tie at the same length, the result is
ambiguous— and ambiguity means no match at all, not a guess. - Archived projects are skipped, and a repository folder with no matching project only ever earns an offer: "this folder is a git repository with no matching project — want to track it?" The companion never creates a project without the user agreeing.
The subtle part is what detection does not do. An explicit choice always wins: if you picked a project with /project, path detection returns nothing. Otherwise the detected project shapes the session's prompt without ever changing the saved active project. The comment in src/core/cli.mjs states it plainly: "an explicit choice always wins; otherwise the working directory can identify the project without changing the saved active project." Your global setting survives a detour into another folder. Detection is also best-effort by contract — in src/core/agent.mjs, resolvePathProject wraps the whole thing in try/catch, because a malformed project store must never stop a session from starting.
One work-log line per turn, zero model calls
After each turn, maintainTurn in src/memory/project-maintenance.mjs writes what the turn did into its project. Its header comment is the design in one sentence: "Deterministic by design: the work log and todo sync cost no model call."
Concretely, a turn that changed files appends exactly one work-log line — a summary like done · 3 file(s) · a.mjs, b.mjs — capped at 400 characters, with at most the first 5 filenames named. The session checklist syncs alongside it: pending or in-progress items become project todos (at most 5 new ones per turn, 500 characters each), and completed or cancelled items close the matching open todo. Everything the automatic pass writes is tagged with its source — { kind: "auto-worklog", sessionId, turnId } — and a retried or replayed turn is harmless, because appendWorkLog refuses to log the same turn twice.
There are guardrails everywhere a silent writer needs them. A turn with no file changes and no checklist movement writes nothing (no-work). Maintenance runs under a file lock, so two processes cannot interleave writes; if the lock is held, the turn is skipped, not queued. If maintenance itself fails, the failure is swallowed into the return value — the turn's reply is never broken by bookkeeping. And the work log itself is bounded: 100 entries, oldest dropped.
The one part that does cost a model call is off by default. PROJECT_INLINE_CONSOLIDATION=on opts into a background extraction pass that reads the session journal (only once it reaches 400 characters) and files decisions and todos — but it runs after the reply is already delivered, so the user never waits for it, and its entries carry the same session tag as everything else automatic.
Undo removes exactly what the session wrote — and nothing else
This is the mechanism I am proudest of, because it falls out of the tagging for free. /project undo calls removeAutoEntries with the current session id, and it deletes only entries whose source kind starts with auto and matches that session: work-log lines, notes, decisions, and todos the session added. Todos the session auto-closed get reopened. Manual entries — the note you typed, the decision you stated — have no auto source, so they survive untouched. The command reports the count honestly: "undid 7 automatic entries (work log 3, todos 2, reopened 1, notes 1, decisions 0)".
So a bad automatic pass is reversible in one command, at exactly the granularity a human can reason about: "everything this session wrote on its own." There is no selective undo, no timestamp guessing, no "which of these 40 lines were automatic?" The tag is the audit trail.
What it does not do
Detection is path-based, so renaming a folder orphans its project until you update the root. The work-log line is audit text, not understanding — "done · 3 file(s)" tells you what changed, never why it mattered. Inline consolidation is opt-in and backgrounded, which means a session's extracted decisions can land after you have already moved on. And session-scoped undo is coarse by design: one command wipes the whole session's automatic trail, not a single turn within it.
That coarseness is the question I keep coming back to. Tell me I am wrong: is session scope the only granularity a human can actually reason about for undo, or should /project undo take a turn id so you can rewind one bad pass without losing the rest of the session's record? Where exactly would per-turn undo break first — the tag is already there, so what is the real cost?
Ankita is open-source (MIT) and local-first — your projects, your models, your machine. The code is at https://github.com/akyourowngames/A.N.K.I.T.A, and I would rather hear that the undo scope is wrong than that the article was nice.
Top comments (0)