Skip to content

RFC: nested provider/product hierarchy for memory_dir categorization #304

@memtomem

Description

@memtomem

Context

memory_dir categorization is currently flat: the server-returned
category field (from categorize_memory_dir in config.py) is one of
user, claude-memory, claude-plans, codex. The Web UI's Sources-tab
memory_dirs panel renders these as four peer groups in a fixed order
(sources-memory-dirs.js: _MEMORY_DIR_CATEGORY_ORDER).

The flat model doesn't match the mental model it describes. claude-memory
and claude-plans aren't peers of codex — they're two products of one
vendor
(Anthropic's Claude tooling). If Claude later exposes a third dir
type (skills? agents? another local surface), the flat bucket list keeps
growing sideways instead of nesting under claude.

The same question is latent for codex — it is currently a single bucket
and a single vendor (OpenAI), but if/when OpenAI adds another local dir
surface, the codexopenai/codex migration would be forced on users
by that external decision rather than chosen now.

Resolved 2026-04-20

Q1–Q6 settled in the working-space issue (#314). Full rationale in the
#314 resolution comment.

Q Resolution
Q1 vendor axis
Q2 user top-level peer
Q3 sibling provider: str field
Q4 parent collapse; user open, vendors closed; no per-child state
Q5 product-level reindex only
Q6 keep codex category; tag provider: "openai"

Corrections to the Phase 1 / Phase 2 sketches below (original sketches
preserved for historical record):

  • Phase 1 mapping table last row: codex → codex is now codex → openai (Q6).
  • Phase 2 parent-label i18n keys: sources.memory_dirs.provider.{user,claude,openai}
    — key matches the server provider wire value, not codex. Category leaf
    codex keeps its existing category.codex key.
  • Phase 2 also adds a value-side vocabulary lock
    _VALID_PROVIDERS = frozenset({"user", "claude", "openai"}) paired with the
    category-axis lock from fix(config): lock provider category vocabulary (RFC #304 prep) #313, so adding a new _CATEGORY_TO_PROVIDER entry
    with an unknown provider value trips an import-time assert.

Phase A follow-up audit (see #314 comment) folded in here:

Why not bundle this with #299 / PR #301

PR #301 (closes #299) shipped the server-side category field deliberately
scoped to classification drift only — no API shape change, one additive
field. Layering a vendor/product hierarchy on top of that would have broken
the thesis in five ways:

  1. API shape change. category: str becomes either a
    {provider, product} object, a path-style string ("claude/memory"),
    or a flat string plus a separate provider field. All three change
    client parsing.
  2. i18n rewrite. Four flat keys (category.user, .claude_memory,
    .claude_plans, .codex) become parent/child two-axis keys; en+ko
    parity re-check + graceful deprecation of the existing keys.
  3. UI complexity. _MEMORY_DIR_CATEGORY_{ORDER,LABEL_KEY,COLLAPSED}
    are flat dicts today. A tree render needs nested <details> with
    parent/child collapse state and a re-scoped "Reindex group" button
    (operates on parent? on child? both?).
  4. Asymmetric leaves. claude has two children (memory, plans),
    codex has none, user has none. Do lone leaves render under a
    synthetic parent, or does the tree mix flat-top-level and nested nodes?
  5. Codex consistency. If the hierarchy axis is "vendor", codex
    eventually becomes openai/codex, forcing a user-facing migration for
    existing configs. Keeping codex flat under a vendor tree breaks
    symmetry; migrating it bloats the PR.

Each of these is a design decision in its own right. Bundling them under a
drift-fix PR would've violated the scope discipline established by the
#295#297#300#301 sequence (one concern per PR).

Design questions (resolved 2026-04-20 — see block above)

Kept for historical record. Q1–Q6 are resolved in the
"Resolved 2026-04-20" block above; Q7 ("Out of scope") is a scope
declaration and is unchanged.

  1. Axis: vendor vs source. Is the parent level "who made this tool"
    (claude, openai, ...) or "who supplied this path" (user-provided
    vs provider-provided)? If "source", user is a peer of claude/
    openai. If "vendor", user has no natural vendor and needs either
    a synthetic user bucket or a mixed tree/flat renderer.
  2. Where does user live? Top-level peer of the vendor groups, or
    an escape-hatch bucket common to all vendors? Peer = simpler tree,
    smaller blast radius.
  3. Wire format. Three options:
    • Keep category: "claude-memory" string, add sibling provider: "claude" string (additive, no type change, easy client migration).
    • Switch to path-style category: "claude/memory" (single field,
      splits client-side).
    • Switch to category: {provider: "claude", product: "memory"}
      object (most type-strict, largest client diff, breaks existing
      consumers).
  4. Collapse semantics. Does collapsing a parent hide all children?
    Does each child still remember its own collapse state? What's the
    default open/closed for parent vs child on first load?
  5. Group reindex scope. "Reindex group" on the parent = reindex all
    children. On a child summary = reindex just that child. Both present
    simultaneously, or parent-only when children exist?
  6. Codex migration. If we adopt a vendor hierarchy, is codex moved
    to openai/codex in the same change, deferred (flat exception), or
    left at its current name to keep the PR minimal?
  7. Out of scope. Gemini is not planned here — its exclusion from
    auto-discovery is driven by per-file-not-per-dir layout and
    secrets-in-parent concerns (auto_ns silently collapses to default when memory_dir basename is non-discriminating (e.g., .../FOO/memory) #296, feedback_namespace_ownership.md),
    not by the hierarchy question. Keep separate.

Proposed sequencing

Break the work into phases so each ships with reviewable scope:

Phase 1 — server-side provider field (additive, non-breaking)

Extend the memory_dir_stats / /api/memory-dirs/status response with a
new provider: str field derived from category:

category provider
user user
claude-memory claude
claude-plans claude
codex codex

Clients ignore the field initially. Risk: YAGNI violation if Phase 2 is
never shipped. Justified only if we commit to following through.

Phase 2 — UI tree render + i18n re-key

  • Switch sources-memory-dirs.js to a two-level render driven by
    providercategory, with single-leaf providers collapsed to a
    single row (no synthetic parent bar for user / codex).
  • Introduce sources.memory_dirs.provider.{user,claude,codex} keys for
    parent labels; keep category.* for leaf labels.
  • Define the deprecation path for existing consumers of the flat
    category field (likely: none in-tree; external MCP clients, if any,
    continue to work because category is kept).

Phase 3 — Codex rename (separate issue)

File under a new issue if/when we decide to normalize codex
openai/codex. Not required for Phases 1–2.

Non-goals for this RFC

Acceptance criteria

  • An owner / agreed-on answer for each of the 7 Design questions above.
  • Phase 1 merged behind a stable provider contract before Phase 2
    begins (so the field isn't churned once UI consumers exist).
  • test_i18n.py parity + placeholder tests continue to pass through
    each phase.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions