v0.2.17

anomalyco/opentuiv0.2.17Aug 24, 2026by het0814

AI Summary

This release focuses on data integrity, introducing atomic OKF bundle replacement and fixing `update_memory` to preserve schema-external metadata. It also improves daily analysis to prevent context window overflow and expands the preference-negation lexicon.

Key Highlights

  • Atomic OKF bundle replacement with cross-process locking
  • OKF round-trip preserves more metadata
  • Bounded session digest for daily summaries
  • `memanto memory sync` always runs a fresh export
  • Expanded preference-negation lexicon

New Features

  • Atomic OKF bundle replacement with cross-process locking
  • OKF round-trip preserves more metadata
  • Bounded session digest for daily summaries
  • `memanto memory sync` always runs a fresh export
  • Expanded preference-negation lexicon

Full Release Notes

# Release Notes for v0.2.17

This release hardens the data-integrity path across recall, migration, and OKF round-trips. Recall no longer silently drops imported memories or mangles filter-only queries; `update_memory` stops clobbering schema-external metadata; OKF exports are now staged and swapped atomically under a cross-process lock so a concurrent import can never observe a half-written bundle. Daily analysis stops overflowing the embedding and LLM context windows on busy days, `memanto memory sync` always writes a fresh export, agent deletion revokes tokens before removing metadata, and the TypeScript SDK's file upload is rebuilt on standard `FormData`.

## Improvements
- **Atomic OKF bundle replacement with cross-process locking** (`memanto/app/services/okf_export_service.py`, `memanto/app/utils/atomic_write.py`, `memanto/cli/migrate/okf_loader.py`)
  - Exports now render into a staging directory and swap into place, instead of overlaying new files onto the existing bundle — deleted or renamed memories can no longer be resurrected on the next import.
  - A failed render leaves the last good bundle intact; a failed final rename restores the previous snapshot from backup.
  - New `okf_bundle_lock()` helper provides a path-scoped, cross-process reader/writer lock (`fcntl` on POSIX, `msvcrt` on Windows with non-blocking retry so contention never hits `LK_LOCK`'s finite retry limit). The loader holds the shared lock across discovery *and* every file read, so an exporter cannot move the bundle aside mid-load.
  - Lock files are intentionally never unlinked — removing one lets a waiter hold a lock on a stale inode while a new caller locks the replacement.
- **OKF round-trip preserves more metadata** (`memanto/app/services/okf_export_service.py`, `memanto/cli/migrate/mappers.py`)
  - `x_memanto` frontmatter now carries `updated_at`, `expires_at`, and `ttl_seconds` alongside the existing `id`/`confidence`/`provenance`/`source`/`status`.
  - Import validates `provenance` against `VALID_PROVENANCE_TYPES` and falls back to `imported` rather than trusting arbitrary strings, and preserves the source `updated_at` instead of stamping migration time.
- **Bounded session digest for daily summaries and conflict scans** (`memanto/app/services/daily_analysis_service.py`)
  - `_truncate_embedding_query()` now builds a 10-chunk digest that samples evenly across the whole session text instead of hard-truncating to the first N tokens, so late-day activity is still represented.
  - The digest is used in the prompt as well as the embedded query, keeping long days inside both the embedding and LLM context windows.
- **`memanto memory sync` always runs a fresh export** (`memanto/cli/client/direct_client.py`, `memanto/cli/client/sdk_client.py`, `memanto/cli/commands/memory_mgmt.py`)
  - The old cache fast-path meant memories written earlier in the same session were missing from the project's `MEMORY.md`. Sync now exports first and only falls back to the previous export when the backend is unreachable, reported as `source: "stale-cache"`.
  - The `--limit` help text and the `cache` source label were updated to match.
- **Expanded preference-negation lexicon** (`memanto/app/services/memory_parsing_service.py`)
  - Added `can not stand` alongside `can't stand`/`cannot stand`, and `detest`/`loathe`/`despise` to the dislike group.
  - Raised the dislike group's weight from 3 to 5 so a negative preference outranks a relationship match on the same sentence.
- **Shared conflicts directory** (`memanto/app/config.py`, `memanto/app/services/daily_analysis_service.py`, `memanto/app/ui/routes/ui_router.py`, `memanto/cli/client/*.py`, `memanto/cli/commands/memory.py`)
  - New `get_conflicts_dir()` / `get_conflict_report_path()` replace five hardcoded `Path.home() / ".memanto" / "conflicts"` constructions, so writers and readers agree on one location under the active data directory.
- **`MemoryError` renamed to `MemoryOperationError`** (`memanto/app/utils/errors.py`)
  - The internal exception no longer shadows Python's builtin `MemoryError`. A `MemoryError = MemoryOperationError` alias is retained for external integrations such as MCP. The HTTP error payload's `error` field now reads `MemoryOperationError`.
- **Pydantic V2 validator migration** (`memanto/app/utils/validation.py`)
  - All 5 `@validator` decorators replaced with `@field_validator` + `@classmethod`, clearing the V1 deprecation warnings. No behavioral change.
- **Web UI typography and loading indicator** (`memanto/app/ui/static/index.html`, `memanto/app/ui/static/ant.svg`, `memanto/app/ui/static/logo.svg`)
  - Switched the body font from Inter to JetBrains Mono via a new `--font-sans` token, and replaced the CSS border spinner with an ant glyph asset (48px in the full-page overlay, 22px inline).

## Bug Fixes
- **`parse_relative_time()` silently returned `None` for natural-language windows** (`memanto/app/utils/temporal_helpers.py`)
  - `"last week"`, `"last month"`, and `"last year"` all fell through to the no-filter sentinel, so callers returned **all** memories instead of recent ones — the timeline-amnesia bug class.
  - Added `last/past week|month|year` (7/30/365 days), `past ...` as a synonym for `last ...`, word-number parsing (`zero`–`twenty`, plus `thirty`/`forty`/`fifty`), and whitespace collapsing so `"last  7  days"` parses. Lookup tables moved to module level.
  - `get_last_n_days`/`get_last_n_hours` are now guarded against `OverflowError` on pathological inputs like `"last 9999999999 days"`, returning `None` instead of crashing.
- **Recall silently dropped memories with unknown confidence** (`memanto/app/services/memory_read_service.py`)
  - Memories with `None`/missing confidence were filtered out whenever `min_confidence > 0`, which primarily hit memories imported via `memanto migrate`. Unknown confidence now fails open, matching how expiration filtering handles unparseable dates. `OverflowError` joins `TypeError`/`ValueError` in the parse guard.
- **Filter-only queries carried a leading space** (`memanto/app/services/memory_read_service.py`)
  - An empty query plus filters produced `" #memory_type:fact"`, which can confuse Moorcheh query parsing. `_build_filtered_query()` now strips and joins correctly for both the empty and non-empty cases.
- **`update_memory()` overwrote schema-external metadata** (`memanto/app/services/memory_read_service.py`, `memanto/app/services/memory_write_service.py`, `memanto/app/constants.py`)
  - `_format_memory_item()` stripped unknown metadata keys (e.g. `original_id` from on-prem `data_store.json`) on read, so the preservation logic in `update_memory()` never received them. Extra keys are now passed through, excluding known schema keys, `memory_type` (a duplicate of `type`), and the new shared `REMOVED_TRUST_FIELDS` frozenset.
  - Carry-forward on update now keys off an explicit `_MEMORY_SCHEMA_FIELDS` set rather than "not already in the document", so an omitted optional field (`tags=[]`, `source_ref=None`) is correctly treated as an intentional clear instead of being restored from the old record.
- **Batch write reported the wrong submitted count** (`memanto/app/services/memory_write_service.py`)
  - `total_submitted` used `len(memories)` rather than `len(results)`, over-reporting when items were rejected before submission.
- **Session summaries logged `session_id: "unknown"`** (`memanto/cli/client/direct_client.py`, `memanto/cli/client/sdk_client.py`)
  - Both clients already resolved a validated session but discarded it and hardcoded `"unknown"` when writing the local Markdown summary. They now log the real `session.session_id` for both `remember` and `batch_remember`.
- **Malformed batch responses were partially tolerated** (`memanto/cli/client/direct_client.py`, `memanto/cli/client/sdk_client.py`)
  - A missing `results` key, or a results array whose length doesn't match the submitted records, now raises `MemoryOperationError` instead of silently pairing memories with `None` results.
- **Namespace limit errors surfaced as generic failures** (`memanto/app/services/agent_service.py`)
  - Moorcheh tier/quota/limit rejections during agent creation now raise a typed `NamespaceError` with the real cause; conflict detection was restructured to catch `ConflictError` alongside message-based matching, and all failures chain the original exception.
- **Renewed session tokens were unreadable by browser clients** (`memanto/app/main.py`)
  - `X-Session-Token` added to the CORS `expose_headers` list — custom response headers are not CORS-safelisted, so header-authenticated clients could not read an auto-renewed token.
- **`memanto session info` compared timezones incorrectly** (`memanto/cli/commands/session.py`)
  - Replaced the deprecated `datetime.utcnow()` and inverted `tzinfo` handling: naive expiry timestamps are now assumed UTC and compared against `datetime.now(timezone.utc)`, instead of stripping tzinfo from aware ones.
- **Negative session durations were accepted** (`memanto/app/services/session_service.py`)
  - `create_session()` now rejects non-numeric or negative `duration_hours` with a `ValueError` instead of minting an already-expired session.
- **LangGraph store crashed on unexpected `list_agents` payloads** (`integrations/langgraph/langgraph_memanto/store.py`, `nodes.py`)
  - The store iterated the response directly; a dict payload (`{"agents": [...]}`) or non-dict entries raised instead of degrading. The shape is now validated and logged, returning `[]` on anything unrecognized.
  - The remember node caps joined message content at 10,000 chars (`MemoryRecord.content` max length), keeping the tail — long conversations were previously rejected and dropped silently.
- **Mem0 category strings split into character tags** (`memanto/cli/migrate/mappers.py`)
  - A single category string was iterated per-character. `_normalize_mem0_categories()` now treats a bare string as one category and handles lists, tuples, and sets uniformly.
- **Supermemory migration lost data** (`memanto/cli/analyze/supermemory_export.py`, `memanto/cli/migrate/mappers.py`, `memanto/cli/migrate/runner.py`)
  - The v4 list endpoint takes `containerTag` (singular), not `containerTags` — the wrong parameter silently under-fetched.
  - Cross-tag deduplication discarded a memory's additional container tags; rows now accumulate a merged `container_tags` list while keeping the singular `container_tag` per bucket for compatibility.
  - The document-chunk fallback only ran when `memories[]` was entirely empty, so unprocessed documents in mixed accounts were dropped. Chunks are now harvested for any document not represented by a mapped memory, and `source_count()` mirrors that logic so the pre-migration summary matches what is actually imported.
- **OKF migration rejected long titles and boolean temporal metadata** (`memanto/cli/migrate/mappers.py`)
  - Titles over `MemoryRecord.title`'s 100-char limit invalidated the whole batch; they are now truncated with the original preserved in the `[Supporting data]` footer.
  - `_parse_dt()` guards against `bool` (which subclasses `int`), where YAML `true` previously became `1970-01-01T00:00:01Z` and could expire a durable memory. `_parse_positive_int()` rejects fractional floats rather than truncating them.
- **Invalid migration export files raised raw tracebacks** (`memanto/cli/commands/migrate.py`, `memanto/app/ui/routes/ui_router.py`)
  - Unreadable or non-JSON export files now produce a clear `ValueError` in the CLI and an HTTP 400 in the UI, instead of an unhandled `JSONDecodeError`/`OSError`.
- **TypeScript SDK file upload rebuilt on `FormData`** (`sdks/typescript/src/index.ts`)
  - Replaced the hand-rolled multipart stream (`createReadStream` + manual boundary/`Content-Length` + `duplex: "half"`) with `openAsBlob()` and standard `FormData`, removing the `escapeMultipartValue` filename-escaping workaround.

## Security
- **Agent deletion now revokes the session token first** (`memanto/app/routes/sessions.py`)
  - `delete_agent()` removed agent metadata before revoking the persisted session, so a failure in local session cleanup left an apparently deleted agent whose old token still authorized requests. The order is now reversed, aborting the deletion if revocation fails.
- **Conflict report paths validated against traversal** (`memanto/app/config.py`)
  - `get_conflict_report_path()` validates `agent_id` against `^[\w\-]+$` and `date` against `^\d{4}-\d{2}-\d{2}$` before joining, replacing the unvalidated f-string path construction used by the CLI clients and UI router.

## Tests
- `tests/test_okf.py` — bundle re-export replaces stale entries, a failed re-export preserves the last good bundle, the loader waits for bundle replacement, single-file loads take the bundle lock, invalid temporal extensions are ignored, and invalid provenance falls back to `imported`.
- `tests/test_unit.py` — file-lock coverage consolidated here, including Windows contention retry without a deadline and non-retry on unexpected errors; `original_id` survives the full read-format-update cycle; batch clients preserve temporal metadata.
- `tests/test_migrate.py` — Mem0 single-category-string handling and the Supermemory container-tag/unprocessed-document fixes (moved and consolidated from `tests/test_migrate_runner.py`).
- `tests/test_temporal_helpers.py` — natural-language and word-number relative-time inputs, numeric last-N-days/hours, and both overflow cases, asserting returned timestamps land within ±1 day/hour of expected.
- `tests/test_daily_analysis_query_length.py` (renamed from the prior query-length file) — a busy-day conflict report keeps the embedded query inside the context window.
- `tests/test_export_resilience.py` — fresh export replaces a stale cache, and the cache is used when the backend is down.
- `tests/test_cli.py` — `load_export` rejects non-object JSON; direct sync exports fresh before copying.
- `tests/test_memory_parsing.py` — expanded negation lexicon and negative-preference ranking.

## Full Changelog
Full Changelog: https://github.com/moorcheh-ai/memanto/compare/v0.2.16...v0.2.17