Compare commits

...

7 Commits

Author SHA1 Message Date
Drew Cain a7e2368f9e chore: update version to 0.21.4 for v0.21.4 release 2026-05-23 14:55:49 -05:00
Sean Campbell c755127317 fix(cli): ignore CancelledError in background task done callback (#839) (#842)
Signed-off-by: rudi193-cmd <rudi193@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 14:27:59 -05:00
Sean Campbell 94c04ee456 fix(mcp): restore write_note overwrite schema for external clients (#818) (#841)
Signed-off-by: rudi193-cmd <rudi193@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 14:19:43 -05:00
Drew Cain d4ed02ba74 docs(core): move release process from CONTRIBUTING.md to AGENTS.md (#846)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:00:01 -05:00
Drew Cain 40ed7129c8 chore: update version to 0.21.3 for v0.21.3 release 2026-05-23 13:46:48 -05:00
Drew Cain c4ef7abff5 test(core): isolate XDG_CONFIG_HOME so host env can't leak into tests (#845)
Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:34:41 -05:00
Paul Hernandez c75f45f3cf Update README.md
update cloud description to remove link to private cloud repo

Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2026-05-23 12:22:18 -05:00
11 changed files with 134 additions and 52 deletions
+25
View File
@@ -253,6 +253,31 @@ async_client.set_client_factory(your_custom_factory)
See SPEC-16 for full context manager refactor details.
### Release Process
Releases are driven by `just release` / `just beta` — never by a bare `git tag`. The recipes bump version metadata, run pre-flight checks, commit, tag, and push. GitHub Actions then publishes to PyPI and updates the Homebrew formula.
**Stable release:**
```
just release v0.21.3
```
The recipe runs `just lint` + `just typecheck`, then updates `__version__` in `src/basic_memory/__init__.py` and `"version"` in `server.json` (MCP registry metadata), commits as `chore: update version to X.Y.Z for vX.Y.Z release`, creates the `vX.Y.Z` tag, and pushes both the commit and the tag to `origin/main`. After the tag lands, the `Release` workflow builds the package, publishes to PyPI, creates the GitHub release with auto-generated notes, and updates the Homebrew formula. The recipe finishes by printing the post-release tasks the workflow doesn't cover.
**Beta release:** `just beta v0.21.3b1` — same flow with a beta-suffixed tag. PyPI consumers install with `pip install basic-memory --pre`.
**Development builds:** every commit to `main` publishes a `0.21.3.dev26+468a22f`-style version to PyPI automatically via `.github/workflows/dev-release.yml`. No human action.
**Do not tag releases by hand.** A bare `git tag vX.Y.Z` skips the in-code version bump. Package metadata is still correct (uv-dynamic-versioning derives it from the git tag) but `basic-memory --version` reports the previous release, which is what happened with v0.21.2 → v0.21.3.
**Post-release tasks** the recipe surfaces but doesn't run:
- `docs.basicmemory.com` — add notes to `src/pages/latest-releases.mdx`
- `basicmachines.co` — bump version in `src/components/sections/hero.tsx`
- MCP Registry — `mcp-publisher publish` from the repo root
See `.claude/commands/release/release.md` (and `beta.md`, `release-check.md`, `changelog.md` alongside it) for the full release + post-release runbook, including the slash commands.
## BASIC MEMORY PRODUCT USAGE
### Knowledge Structure
-34
View File
@@ -224,40 +224,6 @@ See `test-int/BENCHMARKS.md` for detailed benchmark documentation.
- **Fixtures**: Use async pytest fixtures for setup and teardown
- **Markers**: Use `@pytest.mark.benchmark` for benchmarks, `@pytest.mark.slow` for slow tests
## Release Process
Basic Memory uses automatic versioning based on git tags with `uv-dynamic-versioning`. Here's how releases work:
### Version Management
- **Development versions**: Automatically generated from git commits (e.g., `0.12.4.dev26+468a22f`)
- **Beta releases**: Created by tagging with beta suffixes (e.g., `git tag v0.13.0b1`)
- **Stable releases**: Created by tagging with version numbers (e.g., `git tag v0.13.0`)
### Release Workflows
#### Development Builds
- Automatically published to PyPI on every commit to `main`
- Version format: `0.12.4.dev26+468a22f` (base version + dev + commit count + hash)
- Users install with: `pip install basic-memory --pre --force-reinstall`
#### Beta Releases
1. Create and push a beta tag: `git tag v0.13.0b1 && git push origin v0.13.0b1`
2. GitHub Actions automatically builds and publishes to PyPI
3. Users install with: `pip install basic-memory --pre`
#### Stable Releases
1. Create and push a version tag: `git tag v0.13.0 && git push origin v0.13.0`
2. GitHub Actions automatically:
- Builds the package with version `0.13.0`
- Creates GitHub release with auto-generated notes
- Publishes to PyPI
3. Users install with: `pip install basic-memory`
### For Contributors
- No manual version bumping required
- Versions are automatically derived from git tags
- Focus on code changes, not version management
## Creating Issues
If you're planning to work on something, please create an issue first to discuss the approach. Include:
+1 -2
View File
@@ -123,8 +123,7 @@ phone.
time — same files, same format, same wikilinks. Cancel anytime, your data
stays yours.
Built on WorkOS AuthKit, Neon Postgres, and Tigris S3. Platform source at
[basic-memory-cloud](https://github.com/basicmachines-co/basic-memory-cloud).
Built on WorkOS AuthKit, Neon Postgres, and Tigris S3.
### Pricing
+2 -2
View File
@@ -6,12 +6,12 @@
"url": "https://github.com/basicmachines-co/basic-memory.git",
"source": "github"
},
"version": "0.21.1",
"version": "0.21.4",
"packages": [
{
"registryType": "pypi",
"identifier": "basic-memory",
"version": "0.21.1",
"version": "0.21.4",
"runtimeHint": "uvx",
"runtimeArguments": [
{"type": "positional", "value": "basic-memory"},
+1 -1
View File
@@ -1,7 +1,7 @@
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
# Package version - updated by release automation
__version__ = "0.21.1"
__version__ = "0.21.4"
# API version for FastAPI - independent of package version
__api_version__ = "v0"
+4
View File
@@ -447,8 +447,12 @@ class TaskScheduler(Protocol):
def _log_task_failure(completed: asyncio.Task) -> None:
if completed.cancelled():
return
try:
completed.result()
except asyncio.CancelledError:
return
except Exception as exc: # pragma: no cover
logger.exception("Background task failed", error=str(exc))
+1 -5
View File
@@ -35,11 +35,7 @@ async def write_note(
tags: list[str] | str | None = None,
note_type: str = "note",
metadata: Annotated[dict | None, BeforeValidator(coerce_dict)] = None,
# Force/replace are the file-write idioms models default to.
overwrite: Annotated[
bool | None,
Field(default=None, validation_alias=AliasChoices("overwrite", "force", "replace")),
] = None,
overwrite: bool | None = None,
output_format: Literal["text", "json"] = "text",
context: Context | None = None,
) -> str | dict:
+16
View File
@@ -149,6 +149,22 @@ def clean_routing_env(monkeypatch) -> None:
monkeypatch.delenv("BASIC_MEMORY_EXPLICIT_ROUTING", raising=False)
@pytest.fixture(autouse=True)
def isolate_data_dir_env(monkeypatch) -> None:
"""Keep host data-dir env vars from leaking into integration tests.
Why: GitHub Actions Ubuntu runners set ``XDG_CONFIG_HOME=/home/runner/.config``,
and ``resolve_data_dir()`` honors it ahead of ``Path.home() / ".basic-memory"``.
Without clearing it, the MCP tool process reads config.json from the host XDG
path instead of the tmp dir the ``config_manager`` fixture wrote to — so
``test-project`` is missing from ``config.projects``, ``get_project_mode``
falls through to its CLOUD default (#837), and every tool call fails with
"Cloud routing requested but no credentials found."
"""
monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False)
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
POSTGRES_EPHEMERAL_TABLES = [
"search_vector_embeddings",
"search_vector_chunks",
+33 -8
View File
@@ -310,31 +310,40 @@ async def test_write_note_accepts_directory_aliases(mcp_server, app, test_projec
@pytest.mark.asyncio
async def test_write_note_accepts_overwrite_aliases(mcp_server, app, test_project):
"""`force`/`replace` should map to `overwrite`."""
async def test_write_note_overwrite_canonical_via_mcp(mcp_server, app, test_project):
"""Canonical overwrite=True must reach the handler (#818 regression)."""
async with Client(mcp_server) as client:
# First create
await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "Overwrite Alias Note",
"title": "Overwrite Canonical Note",
"directory": "overwrite-test",
"content": "v1",
},
)
# Overwrite using `force` alias
blocked = await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "Overwrite Canonical Note",
"directory": "overwrite-test",
"content": "v2",
},
)
assert "# Error: Note already exists" in blocked.content[0].text
result = await client.call_tool(
"write_note",
{
"project": test_project.name,
"title": "Overwrite Alias Note",
"title": "Overwrite Canonical Note",
"directory": "overwrite-test",
"content": "v2",
"force": True, # alias for overwrite
"overwrite": True,
},
)
assert "Updated note" in result.content[0].text or "Created note" in result.content[0].text
assert "# Updated note" in result.content[0].text
# --- move_note aliases ---
@@ -538,3 +547,19 @@ async def test_aliases_not_advertised_in_schema(mcp_server, app):
assert canonical in props, f"{tool_name}: canonical '{canonical}' missing"
for alias in must_not_have:
assert alias not in props, f"{tool_name}: alias '{alias}' leaked into schema"
# #818: AliasChoices on optional bool broke external-client JSON schema (null-only).
overwrite_schema = tools["write_note"].inputSchema["properties"]["overwrite"]
schema_types: set[str] = set()
if "type" in overwrite_schema:
raw = overwrite_schema["type"]
if isinstance(raw, str):
schema_types.add(raw)
else:
schema_types.update(raw)
for option in overwrite_schema.get("anyOf", ()):
if "type" in option:
schema_types.add(option["type"])
assert "boolean" in schema_types, (
f"write_note overwrite must expose boolean in schema, got {overwrite_schema}"
)
+13
View File
@@ -225,6 +225,19 @@ def isolate_routing_env(monkeypatch) -> None:
monkeypatch.delenv("BASIC_MEMORY_EXPLICIT_ROUTING", raising=False)
@pytest.fixture(autouse=True)
def isolate_data_dir_env(monkeypatch) -> None:
"""Keep host data-dir env vars from leaking into tests.
Why: GitHub Actions Ubuntu runners set ``XDG_CONFIG_HOME=/home/runner/.config``,
and ``resolve_data_dir()`` honors it ahead of ``Path.home() / ".basic-memory"``.
Without clearing it, tests that monkeypatch HOME still see the host XDG path
and assertions against the tmp home directory fail.
"""
monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False)
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
@pytest_asyncio.fixture(autouse=True)
async def cleanup_global_db_after_test() -> AsyncGenerator[None, None]:
"""Close any module-level DB engine created outside fixture ownership."""
+38
View File
@@ -0,0 +1,38 @@
"""Tests for background task done-callback error handling."""
import asyncio
from unittest.mock import patch
import pytest
from basic_memory.deps.services import _log_task_failure
@pytest.mark.asyncio
async def test_log_task_failure_ignores_cancelled_task():
async def slow():
await asyncio.sleep(10)
task = asyncio.create_task(slow())
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
with patch("basic_memory.deps.services.logger.exception") as mock_exc:
_log_task_failure(task)
mock_exc.assert_not_called()
@pytest.mark.asyncio
async def test_log_task_failure_logs_real_exception():
async def boom():
raise ValueError("sync failed")
task = asyncio.create_task(boom())
with pytest.raises(ValueError):
await task
with patch("basic_memory.deps.services.logger.exception") as mock_exc:
_log_task_failure(task)
mock_exc.assert_called_once()
assert "sync failed" in str(mock_exc.call_args)