Skip to content

feat(setup): add Alma target - #175

Merged
Grivn merged 2 commits into
mnemon-dev:masterfrom
mtroym:feat/alma-hooks
Oct 31, 2026
Merged

Grivn merged 2 commits into
mnemon-dev:masterfrom
mtroym:feat/alma-hooks

Conversation

@mtroym

@mtroym mtroym commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

What

Adds an alma target to mnemon setup, on par with the existing 16 targets: installable, ejectable, detectable, and test-covered.

Alma is an Electron desktop AI client on macOS.

Why this target is shaped differently

Alma's hook system does not behave like the lifecycle-hook targets. Hook stdout is buffered and parsed backwards line by line; only the last parseable JSON line is used, and only decision, reason and updatedInput from it have any effect. Everything else printed is discarded.

So porting user_prompt.sh / stop.sh / nudge.sh would produce three silent no-ops. This integration is built around what actually survives.

Design

Alma injects ~/.config/alma/MEMORY.md into the system prompt on every request, and Alma's own prompt tells the model to write to that same file. Two writers on one file is the bug, so mnemon owns exactly one region:

<!-- mnemon:begin -->
...
<!-- mnemon:end -->

Every byte outside those markers is preserved.

mem shim — the single entry point, installed by setup:

  • every read passes --readonly: default recall appends to the oplog, and 10 concurrent recalls produced 4 SQLITE_BUSY failures in testing; with --readonly, 10/10 succeeded
  • direct sqlite access uses file:...?immutable=1, because -readonly and ?mode=ro both fail with unable to open database file (14) on this WAL database
  • id prefixes are resolved in the shim, since mnemon show requires a full UUID (Error: sql: no rows in result set otherwise)
  • --cat is required and general is rejected — it is the unclassified default that turns into noise
  • mnemon recall --cat is accepted but does not filter in 0.2.13 (asking for preference returned decision rows in the same score band), so the shim filters client-side
  • related handles the bare-array response shape, not {"related": [...]}

prime.sh — one app.ready hook, containing no SQL of its own. It calls mem sync, so the hook and a manual sync cannot drift apart. The sync is idempotent: when the block would be unchanged it does not write, leaving the mtime alone so Alma's prompt-prefix cache survives a restart.

SKILL.md — recall triggers (not every turn), the 0–3 writes per conversation rule, marker ownership, and the category/importance guidance.

Eject — removes the block, the skill, the shim and the hook, and keeps backups in the bridge directory.

--target alma requires --global: Alma has no project-local hook configuration.

Verification

  • make test passes.
  • 23 tests in alma_test.go, including: fresh install produces a working skill+shim+hook; re-install is idempotent; unrelated hooks in hooks.json survive; eject removes only mnemon artifacts and preserves user data; the legacy whole-file MEMORY.md format is migrated rather than duplicated; a half-written marker block is refused and logged instead of mangled; the hook contains no SQL and delegates to the shim.
  • End-to-end in a clean HOME: install → mem save → mem find → mem sync → eject leaves no residue.
  • Verified against Alma 0.4.291: GET /api/hooks picks up the config without a restart, and invoking the hook the way Alma does (sh -c + stdin JSON + ALMA_HOOK=true) returns exit 0 with zero bytes on stdout.

Notes

Two behaviours of the shipped app are worth recording for future work on this target: chat.system.transform is listed in Alma's hook UI and handled in applyResult, but nothing ever triggers it, so there is no system-prompt injection channel; and app.willQuit is wrapped in a hard 3000 ms outer race, so no slow work can live there. Neither is used here.

Alma is an Electron desktop AI client whose hook system differs from every
other target here: hook stdout is buffered and only the last parseable JSON
line is read, and only `decision`, `reason` and `updatedInput` from it have any
effect. A reminder-style hook would be a silent no-op, so this integration is
shaped around what actually survives.

The host injects `~/.config/alma/MEMORY.md` into the system prompt on every
request, and Alma's own prompt tells the model to write to that same file. The
integration therefore owns exactly one region of it, delimited by
`<!-- mnemon:begin -->` / `<!-- mnemon:end -->`, and never touches a byte
outside those markers.

- `mem` shim: single entry point for recall and writes. Every read passes
  `--readonly` (default recall appends to the oplog; 10 concurrent recalls
  produced 4 SQLITE_BUSY failures in testing). Direct sqlite access uses
  `file:...?immutable=1`, because `-readonly` and `?mode=ro` both fail with
  "unable to open database file (14)" on a WAL database. Id prefixes are
  resolved here, since `mnemon show` requires a full UUID. `--cat` is required
  and `general` is rejected, so unclassified writes cannot turn into noise.
- `prime.sh`: one `app.ready` hook, with no SQL of its own. It calls
  `mem sync`, so the hook and a manual sync cannot drift. The sync is
  idempotent and leaves the file's mtime alone when the block is unchanged,
  which keeps Alma's prompt prefix cache intact across restarts.
- `SKILL.md`: recall triggers, the 0-3 writes per conversation rule, and the
  marker ownership contract.
- `--eject` removes the block, the skill, the shim and the hook, and keeps any
  backups left in the bridge directory.

Alma has no project-local hook configuration, so `--target alma` requires
`--global`.
The skill alone did not trigger reliably: Alma runs an LLM-based skill
selector, and mnemon lost that vote on most ordinary turns. It also has a
deterministic injection path that this target was not using.

- mark the skill `always-inject: true`, so its description is listed on every
  turn instead of depending on the selector
- move the recall policy into the generated MEMORY.md block: a short statement
  of when to recall, when not to, that a recalled memory is evidence rather
  than an order, and how to write. MEMORY.md is injected in full on every
  request, so this survives the selector entirely.
- cap the block at 8 entries x 100 chars and keep it under 3000 bytes; it is
  charged against every request, so the previous 4.3 KB was too expensive
- generate the static prose with a heredoc: the previous echo chain leaked
  escaped quotes into the output

Adds a test that asserts the block carries the policy, contains no escaped
quotes, and stays within the byte budget.
@Grivn
Grivn merged commit bbc9151 into mnemon-dev:master Oct 31, 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.

2 participants