Skip to content

chore: drop packages/memtomem-stm from monorepo (Phase 3b)#4

Merged
tsdata merged 1 commit intomainfrom
chore/remove-stm-workspace-member
Apr 9, 2026
Merged

chore: drop packages/memtomem-stm from monorepo (Phase 3b)#4
tsdata merged 1 commit intomainfrom
chore/remove-stm-workspace-member

Conversation

@tsdata
Copy link
Copy Markdown
Collaborator

@tsdata tsdata commented Apr 9, 2026

Summary

Phase 3b — final monorepo split. The STM package was extracted into its own repository at https://github.com/memtomem/memtomem-stm via `git filter-repo` (Phase 3a), preserving the relevant squash commits as that repo's history. This PR removes the workspace member from this repo so it becomes a single-package project again.

This is the langchain side of the langchain/langgraph split: this repo is now "memtomem core" only.

Net effect: 85 files changed, +5 / -23128 lines.

Phase 3a (already done, no PR — done out-of-tree)

In a fresh clone of this repo:
```bash
git filter-repo --path packages/memtomem-stm/ \
--path-rename packages/memtomem-stm/:
git remote add origin [email protected]:memtomem/memtomem-stm.git
git push -u origin main
```

Result: `memtomem/memtomem-stm` repo now contains 2 commits (initial v0.1.0 + the Phase 1 surfacing decoupling) with the package laid out at root (`pyproject.toml`, `src/memtomem_stm/`, `tests/`, etc.) — no `packages/` prefix.

What this PR removes

  • `packages/memtomem-stm/` — the entire directory (now lives at `memtomem/memtomem-stm`)
  • `pyproject.toml`:
    • drop `packages/memtomem-stm` from `tool.uv.workspace.members`
    • drop `memtomem-stm` from `tool.uv.sources`
    • drop `packages/memtomem-stm/tests` from `tool.pytest.ini_options.testpaths`
    • collapse the `dev`/`ltm`/`stm` dependency groups into a single `dev` group
  • `.github/workflows/ci.yml` — drop `packages/memtomem-stm/src` from the ruff check + ruff format --check commands
  • `uv.lock` — regenerated by `uv sync` after the workspace shrank

Verification

  • ✅ 850 core tests pass (`uv run pytest -m "not ollama"`)
  • ✅ ruff check + format clean for `packages/memtomem/src`
  • ✅ `uv sync` resolved 73 packages cleanly (memtomem-stm uninstalled)

Cross-repo follow-up (out of scope for this PR)

  • `memtomem/memtomem-stm` needs its own `.github/workflows/ci.yml` and `release.yml` — tracked separately, will land in that repo.
  • Both packages publish to PyPI in Phase 4 with semver compat range (`memtomem-stm` will depend on `memtomem>=0.1,<0.2`).
  • The cross-package boundary is now enforced by the repo split itself: there is no way for STM to import `memtomem.*` directly.

Test plan

  • Core unit tests pass (850)
  • Lint + format clean
  • uv sync clean
  • After merge: confirm CI green on main, then start Phase 4 (PyPI publish)

Phase roadmap

🤖 Generated with Claude Code

Phase 3a extracted packages/memtomem-stm/ into its own repository at
github.com/memtomem/memtomem-stm via git filter-repo, preserving the
two relevant squash commits as that repo's history. This commit
removes the workspace member from this repo so it becomes a single-
package project again (the langchain side of the langchain/langgraph
split: this is "memtomem core").

Changes:
- packages/memtomem-stm/ — entire directory deleted
- pyproject.toml — drop packages/memtomem-stm from tool.uv.workspace
  members and tool.uv.sources; collapse the dev/ltm/stm dependency
  groups into a single `dev` group; drop packages/memtomem-stm/tests
  from pytest testpaths.
- .github/workflows/ci.yml — drop packages/memtomem-stm/src from the
  ruff check and ruff format --check commands.
- uv.lock — regenerated by `uv sync` after the workspace shrank
  (memtomem-stm uninstalled).

Verification:
- 850 core tests pass (`uv run pytest -m "not ollama"`)
- ruff check + format clean for packages/memtomem/src
- uv sync resolved 73 packages cleanly

Cross-repo follow-up:
- memtomem/memtomem-stm now has its own CI/release workflows that
  must be added there. Tracked separately.
- Both packages publish to PyPI in Phase 4 with semver compat range
  (memtomem-stm depends on memtomem>=0.1,<0.2).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@tsdata tsdata merged commit 832017f into main Apr 9, 2026
@tsdata tsdata deleted the chore/remove-stm-workspace-member branch April 9, 2026 12:02
memtomem pushed a commit that referenced this pull request Apr 18, 2026
Reflect the Gemini-regenerated security curation (commit 5fbb47f) in
the handoff doc so a fresh session knows which Phase 2c steps remain.
Updates the phase-progress table, adds a "Security Phase 3a
measurements" subsection (21.9% drift -> H1 supported; intra-vocab
reclassifications to auth/mtls, auth/rbac, networking/tls; incident
subtopic skew), marks next-actions #2-#3 as complete, and points #4
(IDF pre-measure) at the authoritative Gemini batch directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
memtomem pushed a commit that referenced this pull request Apr 18, 2026
11 chunks affected, 13 events / 32 = 40.6% event-count — **Upper-
outlier band (32-45%)** realized per pre-registration, above
observability's 28.1% by 12.5 pp. Pre-registered cell; not a novel
failure mode.

Two systematic Gemini patterns drove 6/13 events (47%):
- Pattern 1: `kubectl logs` diagnostic conflated with
  `observability/logging` subtopic (B3 #2, B5 #1, B6 #2)
- Pattern 2: postmortem genre conflated with
  `incident_response/postmortem` subtopic (B7 #1, B7 #2, B8 #2)

Both are Phase 3b drift-validator "forbidden pair" candidates
(trigger condition k >= 5 topics met at k8s).

Category mix: absent-topic 11, missed secondary 2 (paired with
cross-topic drops on B1 #4 cost_opt→k8s/scaling and B8 #4
observability/metrics→k8s/scheduling), out-of-vocab 0, intra-vocab
0, over-correction 0.

Band-realization sensitivity check: chunk-count drift would be
11/32 = 34.4%, still Upper-outlier. Event-count convention does
not flip the band.

Baseline observation (n=2 event-count at observability + k8s):
drifts span 28.1% to 40.6%. "Prompt-structure drift as constant
~25-30%" hypothesis weakened — topic-level variance appears real.
H1/H2/H3 formal reformulation still deferred to kafka per (A)-
path; no promotion to working hypothesis at n=2.

Post-k8s decision pending divergence (Step 6); handoff Step 3 ✅.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
memtomem pushed a commit that referenced this pull request Apr 18, 2026
Closes wizard-leftovers follow-up #4 (atomic-write migration).

PR #259 introduced _atomic_write_json (tempfile + os.replace with tmp
cleanup on failure) and used it only for `mm config unset`. Two other
paths still called `path.write_text(json.dumps(...))` directly, so a
mid-write failure could corrupt config.json:

- save_config_overrides (config.py) — every CLI set / Web UI PATCH /
  MCP mem_config save ultimately routes here.
- _write_config_and_summary (cli/init_cmd.py) — both normal init and
  --fresh write through the same final statement. The --fresh path
  is the worse case: shutil.copy2 backup could succeed, then
  write_text could fail halfway, leaving a half-written config.json
  next to a valid .bak, requiring manual recovery.

Swapping both call sites to _atomic_write_json gives the same
guarantee --fresh already asked for: either config.json is wholly
updated or it is byte-identical to what it was before the call. The
backup semantics are unchanged (still copy2 before the atomic write),
so the failure-mode outcome improves to "backup == current original,
user can delete backup if desired" from the previous "backup is the
only valid state, original is garbage."

Helper signature (path: Path, data: dict) matches both call sites
exactly — no helper changes needed. The happy-path behaviour is
bit-identical (same json.dump args via the helper), so the existing
8 --fresh tests and every TestSaveConfigOverrides test pass
unchanged.

Redundant path.parent.mkdir call in save_config_overrides removed —
the helper handles it.

Tests (2 new, failure-mode only; happy path is already covered
upstream):

- TestSaveConfigOverrides::test_save_atomic_on_replace_failure —
  mock os.replace to raise; assert existing config.json byte-identical
  and no orphan .config.*.tmp in the parent directory.
- TestFreshFlag::test_fresh_atomic_write_failure_keeps_backup_and_original
  — same mock applied through the --fresh path; assert both the
  pre-fresh original and the .bak-<ts> survive intact.
memtomem added a commit that referenced this pull request Apr 18, 2026
…rite_json (#262)

* fix(config): migrate remaining config.json writes to _atomic_write_json

Closes wizard-leftovers follow-up #4 (atomic-write migration).

PR #259 introduced _atomic_write_json (tempfile + os.replace with tmp
cleanup on failure) and used it only for `mm config unset`. Two other
paths still called `path.write_text(json.dumps(...))` directly, so a
mid-write failure could corrupt config.json:

- save_config_overrides (config.py) — every CLI set / Web UI PATCH /
  MCP mem_config save ultimately routes here.
- _write_config_and_summary (cli/init_cmd.py) — both normal init and
  --fresh write through the same final statement. The --fresh path
  is the worse case: shutil.copy2 backup could succeed, then
  write_text could fail halfway, leaving a half-written config.json
  next to a valid .bak, requiring manual recovery.

Swapping both call sites to _atomic_write_json gives the same
guarantee --fresh already asked for: either config.json is wholly
updated or it is byte-identical to what it was before the call. The
backup semantics are unchanged (still copy2 before the atomic write),
so the failure-mode outcome improves to "backup == current original,
user can delete backup if desired" from the previous "backup is the
only valid state, original is garbage."

Helper signature (path: Path, data: dict) matches both call sites
exactly — no helper changes needed. The happy-path behaviour is
bit-identical (same json.dump args via the helper), so the existing
8 --fresh tests and every TestSaveConfigOverrides test pass
unchanged.

Redundant path.parent.mkdir call in save_config_overrides removed —
the helper handles it.

Tests (2 new, failure-mode only; happy path is already covered
upstream):

- TestSaveConfigOverrides::test_save_atomic_on_replace_failure —
  mock os.replace to raise; assert existing config.json byte-identical
  and no orphan .config.*.tmp in the parent directory.
- TestFreshFlag::test_fresh_atomic_write_failure_keeps_backup_and_original
  — same mock applied through the --fresh path; assert both the
  pre-fresh original and the .bak-<ts> survive intact.

* test(config): add parent-dir structural guard for save_config_overrides

The mkdir removal in the previous commit delegated directory creation
to _atomic_write_json, but every existing TestSaveConfigOverrides
case writes into the isolated fixture's tmp_path — which pytest
creates up front — so the removal was not structurally verified.
Dropping the helper's mkdir too would still pass the full suite.

Add test_save_creates_parent_directory_if_missing that points
_override_path at a nested path whose ancestors don't exist, calls
save_config_overrides, and asserts both the directory and file are
created. Locks in "the helper owns config-dir creation."

---------

Co-authored-by: pandas-studio <[email protected]>
memtomem added a commit that referenced this pull request Apr 25, 2026
…te tags now isolated (#465)

mem_batch_add used to collect the union of every entry's tags after
appending and indexing, then walk every chunk in the target file and
merge the union onto each chunk's metadata.tags. Two failure modes:

1. Cross-entry leak inside the batch: entry A's tags landed on entry
   B's chunk because the broadcast applied the union, not each entry's
   own tag list.

2. Cross-batch leak onto pre-existing chunks: list_chunks_by_source
   returns *all* chunks in the file, so a fresh batch retagged
   memories from earlier sessions that happened to share the file.

The broadcast existed because the chunker did not promote per-entry
``> tags:`` blockquote headers to ChunkMetadata.tags — without it,
mem_batch_add(tags=) had no other way to surface tags to search. PR
#463 made the chunker do exactly that promotion (per-entry, with
mid-section false-positive guards), which makes the broadcast both
redundant and over-applying.

Remove the broadcast block (memory_crud.py:389-415 pre-fix) wholesale.
The test file appended by append_entry already carries
``> tags: ["..."]`` per entry, and the chunker reader picks each up
independently when index_engine.index_file runs.

Adds tests/test_batch_add_tag_isolation.py with three e2e cases on
the real BM25 component stack:

- Tagged + untagged entries in one batch: tagged-entry chunk has its
  declared tag, untagged-entry chunk stays empty (pre-fix it would
  have inherited the union).
- Cross-batch isolation: a pre-existing chunk in the same file is not
  retagged when a tagged batch is appended.
- Two entries with distinct tags keep them distinct (no union).

Follow-up to RFC ``planning/mem-add-tags-blockquote-promote-rfc.md``
§Follow-ups #4. Orthogonal cleanup; closes the broadcast follow-up
without touching the line-drift or chunk_links items.

Co-authored-by: pandas-studio <[email protected]>
Co-authored-by: Claude <[email protected]>
memtomem pushed a commit that referenced this pull request Apr 25, 2026
Two reviewer minors:
- Drop the redundant `DISTINCT` from the inner `SELECT namespace
  FROM chunks` — the outer `UNION` already dedupes, so the inner
  DISTINCT only forced an extra sort pass. Cosmetic perf hit, no
  semantic change.
- Add a symmetric `color == ""` assertion to the chunks-only state
  in the union-states regression test, mirroring the existing
  `description == ""` line. Locks the COALESCE empty-string
  fallback contract for both metadata fields, not just one.

Skipping reviewer #3 (docstring on the empty-string contract) and
#4 (Web `GET /namespaces` e2e for registered-but-empty case) per
the reviewer's own "안 해도 됨" / "별도 follow-up PR 가능" notes.

Co-Authored-By: Claude <[email protected]>
memtomem added a commit that referenced this pull request Apr 25, 2026
)

* fix(storage): list_namespace_meta misses registered-but-empty namespaces

`mm agent register planner && mm agent list` printed `Agents: 0`
even though both `Agent registered: planner` and `Agent registered:
coder` ran successfully. The metadata rows existed; the listing
just never saw them.

Root cause: `list_namespace_meta` queried
  `FROM chunks c LEFT JOIN namespace_metadata m`
which iterated the chunks side, so a `namespace_metadata` row with
no matching `chunks` row was filtered out by `GROUP BY c.namespace`.
Newly-registered agents have no chunks yet, so they were invisible
until someone wrote into their namespace. The Web UI's
`GET /namespaces` route had the same blind spot via the same
storage method.

`shared` continued to appear in `mm agent list` only because the
CLI fetches it separately via `get_namespace_meta(SHARED_NAMESPACE)`
(`agent_cmd.py:168`), which reads `namespace_metadata` directly.

Fix: source `list_namespace_meta` from the union of
`namespace_metadata.namespace` and `chunks.namespace`, then LEFT
JOIN both sides for chunk count + description/color. Three states
now surface correctly:
- metadata only (registered, 0 chunks) — the bug
- chunks only (legacy / un-registered chunks)
- both (registered + has chunks)

Test coverage: existing `test_agent_cmd.py` mocks the storage
method, so the SQL bug never tripped (the
`feedback_storage_artifact_false_pass.md` pattern). Add two
regression tests against the real SQLite backend in
`test_server_tools_org.py::TestNamespace`:

  - `test_list_namespace_meta_includes_registered_empty_namespace`
    pins the exact reproducer (`set_namespace_meta` then
    `list_namespace_meta` with no chunks in between)
  - `test_list_namespace_meta_unions_chunks_and_metadata`
    enumerates all three union states in one fixture so a future
    schema regression on either side fails loudly

Reported by an external tester running the v0.1.28 multi-agent
test scenarios walkthrough at scenario 1 step 1 — exactly the
class of bug the test scenarios doc was written to surface.

Co-Authored-By: Claude <[email protected]>

* fix(storage): polish review nits on PR #473

Two reviewer minors:
- Drop the redundant `DISTINCT` from the inner `SELECT namespace
  FROM chunks` — the outer `UNION` already dedupes, so the inner
  DISTINCT only forced an extra sort pass. Cosmetic perf hit, no
  semantic change.
- Add a symmetric `color == ""` assertion to the chunks-only state
  in the union-states regression test, mirroring the existing
  `description == ""` line. Locks the COALESCE empty-string
  fallback contract for both metadata fields, not just one.

Skipping reviewer #3 (docstring on the empty-string contract) and
#4 (Web `GET /namespaces` e2e for registered-but-empty case) per
the reviewer's own "안 해도 됨" / "별도 follow-up PR 가능" notes.

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: pandas-studio <[email protected]>
Co-authored-by: Claude <[email protected]>
memtomem pushed a commit that referenced this pull request Apr 29, 2026
… in toast, align canonical_root style

Follow-up to PR #549 review:

- **#1 (low-medium)**: drop the hardcoded `_CTX_EMPTY_HINT_META` JS map.
  GET `/api/context/{skills,commands,agents}` now also returns
  `canonical_root: str` and `scanned_dirs: list[str]` (computed once at
  module import from `SKILL_DIRS` / `AGENT_DIRS` / `COMMAND_DIRS`), so the
  empty-state hint pulls from the wire instead of duplicating the
  detector layout client-side. The JS still has a one-line fallback
  (`.memtomem/${type}` + empty list) for older backends, but it never
  fires when the cache-bust takes effect.
- **#2 (cosmetic)**: skills sync response now uses the
  `CANONICAL_SKILL_ROOT = ".memtomem/skills"` constant directly,
  matching the `CANONICAL_AGENT_ROOT` / `CANONICAL_COMMAND_ROOT` pattern
  in agents/commands routes. Drops the `_safe_rel(canonical_skills_root(
  project_root), project_root)` round-trip — both forms resolve to the
  same string today, but a constant won't drift if a future PR adds env
  / scope resolution to `canonical_skills_root()`.
- **#3 (low)**: import-no-runtimes toast renders
  `basename(project_root)` instead of the absolute path. The wire still
  carries the absolute string (consistent with `/api/system/*` and
  useful for debugging / reverse-proxy contexts) but the user-facing
  toast keeps it short — `scanned_dirs` already gives full orientation.
  New `_ctxBasename()` helper handles the POSIX path split.

Cache-bust bumped to `context-gateway.js?v=3`.

Tests:
- New assertions on `TestListSkills.test_empty`,
  `TestListCommands.test_empty`, `TestListAgents.test_empty` verify
  GET response now includes `canonical_root` + `scanned_dirs`.
- `pytest -m "not ollama"` 3152 passed; `ruff check` + `ruff format
  --check` clean.

Skipped per reviewer guidance:
- #4 `Final[str]` → `Literal` / `StrEnum` (mypy is advisory).
- #5 additional positive route assertions for `invalid_name` /
  `already_imported` / `toml_parse_error` (route layer is a trivial
  dict comprehension; covered at the core layer).
- #6 Korean `<이름>` literal angle-bracket (intentional placeholder
  guide, not an unsubstituted variable).

Co-Authored-By: Claude <[email protected]>
memtomem added a commit that referenced this pull request Apr 29, 2026
* fix(web): surface "why nothing happened" on Skills/Commands/Agents Sync+Import

Users reported "Sync 도 Import 도 동작이 이상함" — both buttons in
mm web → Settings → Skills/Commands/Agents looked successful (a green
toast popped) but nothing actually moved on disk. The root cause was a
shape mismatch in the response handling, not a wiring bug:

- For skills, the sync response carries `skipped` (e.g. "no canonical
  skills") but never `dropped`. The client only read `dropped`, so the
  skipped reason was silently dropped on the floor.
- The import response carried no metadata at all, so a 0-imported,
  0-skipped result (the common case when no `.claude/skills/` exists in
  the project) just rendered "Import completed" with no clue that the
  scanner found nothing because the directories didn't exist.

This change keeps cwd-bound single-project behavior unchanged and adds
machine-readable plumbing the UI can match against:

- All three context types' `Sync` and `Import` core layers
  (`memtomem.context.{skills,commands,agents}`) now record skipped items
  as `(name, reason, reason_code)` triples. Reason codes come from a new
  `memtomem.context._skip_reasons` module: `no_canonical_root`,
  `unknown_runtime`, `parse_error`, `invalid_name`, `already_imported`,
  `canonical_exists`, `toml_parse_error`. Human `reason` strings stay
  for CLI/log output.
- Web route handlers (`web/routes/context_{skills,commands,agents}.py`)
  surface `reason_code` per skipped item, plus `canonical_root` on sync
  responses and `project_root` + `scanned_dirs` on import responses.
- The web client (`context-gateway.js`) reads `data.skipped` (in
  addition to `data.dropped` for commands/agents field-level drops),
  matches on `reason_code === "no_canonical_root"` to show
  "No canonical {type} under {canonical}. Create one first." instead of
  a generic success, and on a 0/0 import shows "No runtime {type} found
  in {project_root}. Scanned: {scan_dirs}." so users see exactly which
  paths the detector inspected.
- The empty list state hint now points at the canonical and runtime
  paths instead of "Create one or import from existing runtimes." — a
  fresh user can drop a `SKILL.md` into the right directory without
  guessing.
- Three new placeholder-form i18n keys (`empty_hint`,
  `import_no_runtimes`, `sync_empty_canonical`) cover all three types
  via `{type}` / `{canonical}` / `{root}` / `{scan_dirs}` substitution
  rather than 9 type-specific keys.
- `context-gateway.js?v=2` cache-bust.

All response changes are additive — existing clients that only read
`imported`/`generated`/`skipped[].name|runtime|reason` keep working
unchanged. The CLI (`mm context skills/agents/commands ...`) and the
MCP `context` tool also unpack the new triple shape via `_code` discard
so their human-readable output is byte-identical.

Tests:
- 3 core test suites (`test_context_{skills,commands,agents}.py`)
  updated to the 3-tuple shape.
- New web route assertions verify `reason_code`, `canonical_root`,
  `project_root`, and `scanned_dirs` are present in sync/import
  responses for the empty-canonical and empty-runtime paths.
- Full suite green: `pytest -m "not ollama"` 3152 passed; `ruff check`
  + `ruff format --check` clean.

Co-Authored-By: Claude <[email protected]>

* fix(web/context): address review — surface scan dirs on GET, basename in toast, align canonical_root style

Follow-up to PR #549 review:

- **#1 (low-medium)**: drop the hardcoded `_CTX_EMPTY_HINT_META` JS map.
  GET `/api/context/{skills,commands,agents}` now also returns
  `canonical_root: str` and `scanned_dirs: list[str]` (computed once at
  module import from `SKILL_DIRS` / `AGENT_DIRS` / `COMMAND_DIRS`), so the
  empty-state hint pulls from the wire instead of duplicating the
  detector layout client-side. The JS still has a one-line fallback
  (`.memtomem/${type}` + empty list) for older backends, but it never
  fires when the cache-bust takes effect.
- **#2 (cosmetic)**: skills sync response now uses the
  `CANONICAL_SKILL_ROOT = ".memtomem/skills"` constant directly,
  matching the `CANONICAL_AGENT_ROOT` / `CANONICAL_COMMAND_ROOT` pattern
  in agents/commands routes. Drops the `_safe_rel(canonical_skills_root(
  project_root), project_root)` round-trip — both forms resolve to the
  same string today, but a constant won't drift if a future PR adds env
  / scope resolution to `canonical_skills_root()`.
- **#3 (low)**: import-no-runtimes toast renders
  `basename(project_root)` instead of the absolute path. The wire still
  carries the absolute string (consistent with `/api/system/*` and
  useful for debugging / reverse-proxy contexts) but the user-facing
  toast keeps it short — `scanned_dirs` already gives full orientation.
  New `_ctxBasename()` helper handles the POSIX path split.

Cache-bust bumped to `context-gateway.js?v=3`.

Tests:
- New assertions on `TestListSkills.test_empty`,
  `TestListCommands.test_empty`, `TestListAgents.test_empty` verify
  GET response now includes `canonical_root` + `scanned_dirs`.
- `pytest -m "not ollama"` 3152 passed; `ruff check` + `ruff format
  --check` clean.

Skipped per reviewer guidance:
- #4 `Final[str]` → `Literal` / `StrEnum` (mypy is advisory).
- #5 additional positive route assertions for `invalid_name` /
  `already_imported` / `toml_parse_error` (route layer is a trivial
  dict comprehension; covered at the core layer).
- #6 Korean `<이름>` literal angle-bracket (intentional placeholder
  guide, not an unsubstituted variable).

Co-Authored-By: Claude <[email protected]>

* refactor(context): narrow reason_code to Literal SkipCode

Tightens the typing on the third element of `(name, reason, reason_code)`
triples produced by `SkillSyncResult.skipped`, `CommandSyncResult.skipped`,
`AgentSyncResult.skipped`, and `ExtractResult.skipped`. The new
`memtomem.context._skip_reasons.SkipCode` is a `Literal` of the seven
codes already enumerated in that module, so a typo at the construction
site fails type-check instead of slipping through to the wire.

Also strengthens the import-empty web-route tests for skills, commands,
and agents to assert `data["project_root"] == str(tmp_path)` instead of
just key presence — confirms the response actually echoes the
fixture's project root rather than some unrelated path.

No runtime behavior change; addresses follow-up polish flagged in the
review of this PR.

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: pandas-studio <[email protected]>
Co-authored-by: Claude <[email protected]>
memtomem pushed a commit that referenced this pull request Apr 29, 2026
Self-review of PR #553 surfaced five real issues — fixing them keeps
the surface honest about what's actually wired up.

- Drop dead helpers in ``context/projects.py`` (#1):
  - ``_is_eaccess`` was never called.
  - ``is_project_scope_id`` claimed to support route error formatting
    but no route used it.
  - ``_new_temp_known_projects_path`` claimed test convenience but no
    test imported it.
  - ``errno`` and ``tempfile`` imports follow.
- Drop ``ProjectScope.counts`` dataclass field (#2). Discovery never
  populated it; the route layer (``_scope_to_dict``) always overrides
  via ``_counts_for(root)``. Forward-design pollution.
- ``POST /api/context/known-projects`` now returns ``warning_code:
  "no_runtime_marker"`` alongside the prose ``warning`` (#4). Matches
  PR1's (#549) machine-readable ``reason_code`` pattern so client
  matching is i18n-stable; the prose stays for back-compat.
- ``_counts_for`` gains a cost comment (#3): 3 × (canonical scan +
  N runtime scans) per scope on every projects GET. Acceptable at
  <30 scopes; cache when discovery growth pushes that ceiling.
- JS scope badge no longer carries a redundant ``data-i18n`` attribute
  (#5). The inline ``t()`` already renders text at construction; the
  attribute would let the i18n DOM walker re-translate and clobber.
- ``ctx-scope-summary`` now carries a ``title="${scope.root}"`` (#8) so
  same-name scopes (``Edu/inflearn`` vs ``Work/inflearn``) disambiguate
  on hover without bloating the visible label.

Cache-bust: ``context-gateway.js?v=4`` → ``?v=5``.

Tests updated: ``test_post_warns_on_missing_marker`` asserts the new
``warning_code``; ``test_post_no_warning_when_marker_present`` asserts
both fields are absent. 110/110 green; ruff + format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
memtomem added a commit that referenced this pull request Apr 29, 2026
…#553)

* feat(web): multi-project read-only discovery for context-gateway tabs

PR2 of the multi-project context UI series — see
``memtomem-docs/memtomem/planning/multi-project-context-ui-rfc.md``.

A user running ``mm web`` from the ``memtomem`` repo could previously
only see skills / commands / agents under that one cwd; ``~/Edu/inflearn/
.claude/skills/`` was invisible without restarting the server. PR2 adds
discovery for additional project roots (server cwd, user-registered
``known_projects.json``, opt-in ``~/.claude/projects/`` reverse-decode)
and renders one collapsible ``<details>`` group per scope inside each
tab. Mutating routes stay cwd-only — multi-scope writes ship in PR3.

Backend
-------

- ``memtomem.context.projects`` is the new discovery module:
  - ``ProjectScope`` carries ``{scope_id, label, root, tier, sources,
    missing, experimental, counts}``.
  - ``compute_scope_id`` derives ``"p-" + sha256(case-normalized
    Path.resolve())[:12]`` so refresh / restart preserve the id (RFC
    §Decision 4). On macOS the input is also lowercased: APFS is
    case-insensitive but ``Path.resolve()`` does not canonicalize case.
  - ``KnownProjectsStore`` writes ``~/.memtomem/known_projects.json``
    via ``tempfile + os.replace`` plus a ``.<name>.lock`` sidecar
    fcntl lock — locking the data file directly does not survive
    ``os.replace`` (PR #548 issue).
  - ``discover_project_scopes`` unions cwd + known-projects + (opt-in)
    claude-projects, dedupes by resolved path, and clears
    ``experimental`` on union with a trusted source.
- ``ContextGatewayConfig`` (new) carries ``known_projects_path``,
  ``experimental_claude_projects_scan: bool = False``, and
  ``user_tier_enabled: bool = False`` (forward-compat for PR3).

Routes
------

New ``web/routes/context_projects.py``:

- ``GET /api/context/projects`` — full scope list with per-type counts.
- ``POST /api/context/known-projects`` — register; absolute + ``is_dir()``
  validation; warning (HTTP 200) when no ``.claude``/``.gemini``/``.agents``/
  ``.memtomem`` marker is present so users can pre-register an empty
  checkout.
- ``DELETE /api/context/known-projects/{scope_id}`` — drop, including
  stale entries (matching is path-derived).
- ``resolve_scope_root`` is the dependency the existing
  ``/api/context/{skills,commands,agents}`` GETs now use; without
  ``?scope_id=`` it falls back to the server cwd so PR1's mutating
  cwd flow keeps working unchanged. Unknown / stale scope_id → 404.

UI
--

Each tab gains an "Add Project" button; the list area renders one
``<details class="ctx-scope-group">`` per scope, lazy-fetching items
on expand. Server CWD opens by default; non-cwd scope items render as
read-only cards (PR2 keeps mutating buttons targeting cwd only). Each
non-cwd group has an ✕ button that calls DELETE
``/api/context/known-projects/{scope_id}``.

Three new i18n keys (placeholder-form, en + ko parity).

Tests
-----

PR2 minimum-bar from RFC §Test obligations:

- ``tests/test_context_projects.py`` — scope_id stability + collision
  sanity, trailing-slash invariance, macOS case-insensitivity,
  symlink dedup, ``experimental`` flag clears on union, both
  ``experimental_claude_projects_scan`` defaults, ``known_projects.json``
  corruption / unknown-version recovery, stale-entry removal,
  multiprocessing.Process atomic-write race.
- ``tests/test_web_routes_context_projects.py`` — HTTP shape, unknown
  scope_id 404, POST 400 paths (relative / nonexistent / file),
  marker warning, idempotent registration, DELETE round-trip + 404.

Manual smoke
------------

``HOME=/tmp/.../home uv run mm web --port 8088``: GET projects shows
cwd; POST /tmp returns 200 + warning; GET projects shows two scopes
(``Server CWD`` + ``tmp``); skills?scope_id=p-bogus → 404; DELETE
round-trip works; second DELETE → 404.

Refs RFC ``multi-project-context-ui``; gates PR3 (multi-project
mutating + Claude user-tier).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* review fixes: drop dead helpers, add warning_code, summary tooltip

Self-review of PR #553 surfaced five real issues — fixing them keeps
the surface honest about what's actually wired up.

- Drop dead helpers in ``context/projects.py`` (#1):
  - ``_is_eaccess`` was never called.
  - ``is_project_scope_id`` claimed to support route error formatting
    but no route used it.
  - ``_new_temp_known_projects_path`` claimed test convenience but no
    test imported it.
  - ``errno`` and ``tempfile`` imports follow.
- Drop ``ProjectScope.counts`` dataclass field (#2). Discovery never
  populated it; the route layer (``_scope_to_dict``) always overrides
  via ``_counts_for(root)``. Forward-design pollution.
- ``POST /api/context/known-projects`` now returns ``warning_code:
  "no_runtime_marker"`` alongside the prose ``warning`` (#4). Matches
  PR1's (#549) machine-readable ``reason_code`` pattern so client
  matching is i18n-stable; the prose stays for back-compat.
- ``_counts_for`` gains a cost comment (#3): 3 × (canonical scan +
  N runtime scans) per scope on every projects GET. Acceptable at
  <30 scopes; cache when discovery growth pushes that ceiling.
- JS scope badge no longer carries a redundant ``data-i18n`` attribute
  (#5). The inline ``t()`` already renders text at construction; the
  attribute would let the i18n DOM walker re-translate and clobber.
- ``ctx-scope-summary`` now carries a ``title="${scope.root}"`` (#8) so
  same-name scopes (``Edu/inflearn`` vs ``Work/inflearn``) disambiguate
  on hover without bloating the visible label.

Cache-bust: ``context-gateway.js?v=4`` → ``?v=5``.

Tests updated: ``test_post_warns_on_missing_marker`` asserts the new
``warning_code``; ``test_post_no_warning_when_marker_present`` asserts
both fields are absent. 110/110 green; ruff + format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: pandas-studio <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
memtomem added a commit that referenced this pull request Apr 30, 2026
…item bundle) (#579) (#587)

* feat(web): Index folder panel — surface one-shot vs persistent (#579)

The folder Index button runs a one-shot scan; persistent re-indexing
requires registering the path under Sources. PR #575 unified the Index
tab modes but left this difference silent — users who wanted automatic
re-indexing had no in-panel cue that they should look elsewhere.

Add an inline hint at the top of the folder panel ("One-shot scan. For
automatic re-indexing, use Sources → Add path."). The link element is
split out as a sibling of the help text so the i18n updater (which
overwrites textContent on every lang toggle) can't strip the click
handler from a nested anchor.

The Index success toast also gains a "Register as Source" action button
that runs the same Sources-tab navigation helper, so the cue is reachable
both before and after the scan.

Co-Authored-By: Claude <[email protected]>

* feat(web): Compose — surface target file (#579)

Make the Compose mode's `Target file` semantics visible in two places:

- Placeholder: previously the generic `path/to/memories.md`. Now
  `~/.memtomem/memories/2026-04-30.md (leave blank for auto)` — the
  default location backend code in `add_memory()` already uses, with
  today's date computed at load time so the cue stays accurate.

- Add success toast: previously "Saved — N chunks indexed", agnostic to
  where the content went. Now "Saved to ~/.memtomem/memories/…/file.md
  — N chunks indexed", echoing the actual path returned by the API.

The placeholder is split into i18n prefix + `{date}` runtime + i18n
suffix to dodge the lang-toggle path: i18n.applyDOM rewrites
`data-i18n-placeholder` textContent on every switch and would otherwise
clobber a JS-templated value back to the raw `{date}` token. The input
keeps a static placeholder fallback (`~/.memtomem/memories/`) for the
sub-second window before settings-config.js's helper runs.

The toast path-tildify helper (`tildifyPath`) collapses the
host-specific `$HOME` prefix the server sends back to `~`. The browser
doesn't know `$HOME`, so the helper anchors on the `.memtomem/` segment
that backend write paths consistently use. Reused in #4.5a.

The deprecated `toast.saved_n_indexed` key is removed from both locales
(unused after this commit) and dropped from the i18n required-set test.

Co-Authored-By: Claude <[email protected]>

* feat(web): Upload — echo saved path in success toast (#579)

Upload silently writes to ~/.memtomem/uploads/, then on collision
suffixes the destination filename with the existing file's mtime
(system.py:862). Until now the success toast only said how many chunks
were indexed; users had no way to see where the file actually landed,
or that a rename had occurred.

Add `path: str | None = None` to UploadFileResult and have the upload
route echo the resolved `dest` (post-rename) so the client doesn't have
to guess. The toast prefers the new path-aware key
(`toast.upload_complete_with_path` — "Indexed N chunks → saved to
~/.memtomem/uploads/foo.md"), falling back to the count-only message if
no path is present (e.g., all-error batch). The path-tildify helper
introduced in the previous commit collapses $HOME to ~ for display.

The schema change is purely additive (new optional field, default
None), so existing callers and tests continue to work unchanged.

Co-Authored-By: Claude <[email protected]>

* feat(web): header sys-info chip jumps to Settings → Config (#579)

The "provider/model · backend" chip in the header is the only place
that surfaces the embedding pipeline and storage backend on every tab.
Users debugging "why is search slow" or "why are embeddings cleared"
have to recall that the matching settings live under Settings → Config
and click through manually.

Make the chip clickable: cursor changes on hover/focus, click +
keyboard activation (Enter/Space) navigate to Settings tab and select
the Config section. The tooltip now ends with "Open Embedding & Storage
settings" (i18n'd) and an aria-label exposes the same hint to screen
readers; both refresh on language toggle alongside the existing
detail lines (provider/model/dim, backend).

Co-Authored-By: Claude <[email protected]>

* style(web): Compose textarea — double the default height (#579)

5 rows is enough to write a tag list, not enough to write the kind of
freeform memo people actually paste into Compose. Lift `min-height`
from 120 to 240 px (≈10 rows) so the textarea matches a real writing
surface; users can still drag the resize handle in either direction.

Co-Authored-By: Claude <[email protected]>

* style(web): Index Extensions hint — drop callout chrome, add ⓘ glyph (#579)

The Extensions/max-chunk hint at the bottom of the folder panel is read
as data (current config values), not as a callout that demands the
user's attention. Inheriting the shared .config-hint background +
border made it look heavier than the surrounding inputs.

Override the styling for `#index-config-hint` only (the home and
namespace hints — `#home-config-info`, `#ns-config-info` — keep their
existing chrome) to a plain muted line, and add the ⓘ marker via CSS
`::before` so it's single-source-of-truth in the stylesheet regardless
of which writer populates `textContent`.

Co-Authored-By: Claude <[email protected]>

* copy(web): Compose target file label — clarify "save location" (#579)

"Target file" alone reads as if it could be a path on disk, on the
filesystem, or a virtual id — the user has no anchor for what's being
asked of them. Append "(save location)" / "(저장 위치)" to the label so
the placeholder hint and saved-path toast (introduced earlier in this
bundle) reinforce the same idea: this input controls where the memory
is stored under ~/.memtomem/memories/.

Markup unchanged — just an i18n string update in both locales.

Co-Authored-By: Claude <[email protected]>

* fix(web): i18n init — fire langchange after locale load (#579)

Several JS-owned dynamic strings populate via listeners — Compose
placeholder (settings-config.js _refreshAddFilePlaceholder) and the
header-chip aria-label / jump-hint title (_syncHeaderConfig) — read
t() at the moment they run. settings-config.js's module-level
fetchServerConfig() can race ahead of app.js's `await I18N.init()`, so
those listeners can fire before the locale cache is populated and end
up writing the raw i18n key (e.g. `index.add_file_placeholder_prefix`)
to user-visible UI. Surfaced by the smoke for #579: the Compose
placeholder showed `index.add_file_placeholder_prefix2026-04-30
index.add_file_placeholder_suffix` and the header chip's aria-label
held `header.sys_info_jump_title` until the user toggled language.

Have I18N.init() dispatch a single langchange event after the locale
cache is filled. The same listeners then re-read t() with real
translations and overwrite their stale raw-key state. The bumped query
string on i18n.js (?v=2 → ?v=3) busts browser disk cache so the new
init body actually loads.

Co-Authored-By: Claude <[email protected]>

---------

Co-authored-by: pandas-studio <[email protected]>
Co-authored-by: Claude <[email protected]>
memtomem added a commit that referenced this pull request Apr 30, 2026
#589)

The Index tab's mode toggle (folder / upload / compose) used the global
.btn-ghost.btn-active outline, while the main .tab-nav above already
uses a bottom-border for its active tab. Both rendering as a blue
highlight with different decoration (outline vs underline) weakens the
parent/child hierarchy between main tabs and sub-tabs.

Switch the mode toggle's active state to the same bottom-border
convention used by .sources-vendor-tab so both rows read as tabs but at
different scales — main tabs stay larger and prominent, the mode toggle
reads as subordinate.

Refs #582 (Prev #4).

Co-authored-by: pandas-studio <[email protected]>
Co-authored-by: Claude <[email protected]>
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