Skip to content

feat: a memory config block, and a searchable config dialog - #103

Merged
serkanalgur merged 1 commit into
mainfrom
feat/config-memory-and-search
Sep 30, 2026
Merged

serkanalgur merged 1 commit into
mainfrom
feat/config-memory-and-search

Conversation

@serkanalgur

Copy link
Copy Markdown
Owner

What

Persistent memory had no config surface at all. The block on the wider embedder type (NexusConfig) was removed years ago because nothing read it, its database path and entry cap were hardcoded in memory-store.ts, and the store was frozen at construction. The config dialog discovers its rows by walking the config, so adding the block is what makes it editable — there was nothing to wire up in the dialog itself.

Adds memory: { enabled, storage, maxEntries } as the ninth block, and three improvements to the dialog.

memory

field when it takes effect
enabled next recall — a per-call gate on recallForTask
storage next write — the path is resolved per use
maxEntries next write — a pure read, no reopen

enabled defaults to true, because recall is already unconditional on both spawn paths. Defaulting it to false would silently delete a running feature for every existing user on upgrade. It does not gate store construction: off means "read nothing", not "no database".

storage and maxEntries used to require a restart, which made them a knob that reads like a control and is inert — the exact reason the original memory block was removed. They now resolve per use. The swap is ordered open-then-close-then-publish and contains no await, so concurrent recallForTask calls cannot observe a partial state; a path that cannot be opened throws with the previous database still open and still current, rather than leaving the store with no handle.

Config dialog

  • Search on the hub, matching block names and setting names, landing straight on the field's editor. One-shot prompt rather than a persistent filter, because the host owns the select's lifetime and has no onInput — a live filter would put query state in the adapter where no test can reach it. A query matching nothing is a selectable row, not a zero-option dialog, so the screen is escapable and the quoted query makes a typo visible.
  • Dirty counts per block, and a hub header naming how many changes are not yet written. Every marker is a glyph and a number — none depends on colour.
  • Single-keystroke booleans. A boolean now toggles on its own screen instead of opening a second dialog to pick between two rows. The way-out row sits beneath the switch, so a mutating key is never the only key on a screen.

Three bugs fixed along the way

  1. A relative memory.storage opened the model picker, and picking a model wrote a provider/id into the database path. A field is now a model reference because of what it is, never because a string contains a slash. This also fixes customRoles[].prompt — role prompts are paragraphs that nearly always name a file, so the common case misclassified — and customRoles[].displayName.

  2. memoryBase was seeded by spreading a literal whose keys were present but undefined, which deleted storage and maxEntries from getConfig() and from every file getSaveableConfig() wrote. 1373 green tests missed it because they all constructed a bare NexusConfigManager; the orchestrator-configured path is now covered.

  3. The Save row claimed a count it did not deliver — "Write 3 changes to disk" while the save wrote nine blocks. The number described what the user touched, not what was written, so the wording no longer asserts it is a count of the write.

Behaviour change to be aware of

Under models, a value with no slash (a typo, or a note parked in the block) now opens the provider picker rather than a free-text box. models is an open keyed block whose leaves are model references by definition, and the picker is what you want for a wrong model id. Restoring the old fallback would need a shape check scoped to models alone — I would not recommend it.

How to verify

bun test          # 1386 pass, 0 fail
bunx tsc --noEmit

Then /nexus config (or /nc):

  • the hub lists nine blocks, including memory; the header shows Nexus configuration
  • Search settings is the second row; search cost and you get budget.* fields — pick one and it opens its editor directly
  • toggle memory.enabled; it flips in place on its own screen, gets a *, and the hub header reads 1 change not written
  • set a relative storage (e.g. ./tmp/m.db), save, and confirm the row is a free-text prompt, not a provider list
  • save, then check memoryStore.path and getConfig().memory.storage agree — they used to be able to disagree

Notes

  • The models/budget/... merge stays field-by-field, so a level setting only memory.enabled cannot blank storage.
  • docs/API.md is updated: the block count, the schema fence (which a test parses structurally), and the paragraph claiming a per-scope cap.

The config dialog discovers its rows by walking the config, so any block
that exists is already editable. But persistent memory was not reachable
at all: it had no block (the one on the wider embedder type was removed
because nothing read it), its db path and entry cap were hardcoded, and
the store was frozen at construction. A ninth block fixes that.

memory: { enabled, storage, maxEntries }

  enabled is a per-call gate on recallForTask, so turning recall off
  takes effect on the next task and nothing else. It defaults to true
  because recall is already unconditional today — false would silently
  delete a running feature on upgrade.

  storage and maxEntries resolve per use rather than at construction.
  Editing storage in nexus.jsonc and saving now moves the database
  without a restart; the old file is left intact, and a path that cannot
  be opened throws with the previous database still current rather than
  leaving the store with no handle. The swap is ordered open-then-close
  and contains no await, so concurrent recalls cannot observe a partial
  state. The cap is a pure read and needs no reopen.

Three fixes fell out of the same work:

  A relative memory.storage opened the grouped model picker, and picking
  a model wrote a provider/id into the database path. A field is now a
  model reference because of what it is, never because a string contains
  a slash — which also fixes customRoles[].prompt, whose text nearly
  always names a file, and customRoles[].displayName.

  memoryBase was seeded by spreading an object literal whose keys were
  present but undefined, so storage and maxEntries were dropped from
  getConfig() and from every file getSaveableConfig() wrote. 1373 green
  tests missed it because they all built a bare NexusConfigManager; the
  orchestrator-configured path is now covered.

  The Save row said "Write 3 changes to disk" while the save wrote nine
  blocks. The number described what the user touched, not what was
  written, so the wording no longer claims to be a count of the write.

Config dialog:

  Search on the hub, matching block names and setting names, landing
  straight on the field's editor. A query that matches nothing is a
  selectable row, not a zero-option dialog, so the screen is escapable
  and a typo is visible in the quoted query.

  Dirty counts per block, and a hub header naming the number of changes
  not yet written. Every marker is a glyph plus a number, so none of
  them depends on colour.

  A boolean toggles on its own screen in one keystroke, with a visible
  way out beneath it, instead of opening a second dialog to pick from
  two rows. One mutating row is never the only row on a screen.

1386 tests pass.
@serkanalgur
serkanalgur merged commit ad908e0 into main Sep 30, 2026
4 checks passed
@serkanalgur
serkanalgur deleted the feat/config-memory-and-search branch September 30, 2026 08:37
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