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 (fcntlon POSIX,msvcrton Windows with non-blocking retry so contention never hitsLK_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_memantofrontmatter now carriesupdated_at,expires_at, andttl_secondsalongside the existingid/confidence/provenance/source/status.- Import validates
provenanceagainstVALID_PROVENANCE_TYPESand falls back toimportedrather than trusting arbitrary strings, and preserves the sourceupdated_atinstead 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 syncalways 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 assource: "stale-cache". - The
--limithelp text and thecachesource label were updated to match.
- The old cache fast-path meant memories written earlier in the same session were missing from the project's
- Expanded preference-negation lexicon (
memanto/app/services/memory_parsing_service.py)- Added
can not standalongsidecan't stand/cannot stand, anddetest/loathe/despiseto 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.
- Added
- 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 hardcodedPath.home() / ".memanto" / "conflicts"constructions, so writers and readers agree on one location under the active data directory.
- New
MemoryErrorrenamed toMemoryOperationError(memanto/app/utils/errors.py)- The internal exception no longer shadows Python's builtin
MemoryError. AMemoryError = MemoryOperationErroralias is retained for external integrations such as MCP. The HTTP error payload'serrorfield now readsMemoryOperationError.
- The internal exception no longer shadows Python's builtin
- Pydantic V2 validator migration (
memanto/app/utils/validation.py)- All 5
@validatordecorators replaced with@field_validator+@classmethod, clearing the V1 deprecation warnings. No behavioral change.
- All 5
- 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-sanstoken, and replaced the CSS border spinner with an ant glyph asset (48px in the full-page overlay, 22px inline).
- Switched the body font from Inter to JetBrains Mono via a new
Bug Fixes
parse_relative_time()silently returnedNonefor 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 forlast ..., word-number parsing (zero–twenty, plusthirty/forty/fifty), and whitespace collapsing so"last 7 days"parses. Lookup tables moved to module level. get_last_n_days/get_last_n_hoursare now guarded againstOverflowErroron pathological inputs like"last 9999999999 days", returningNoneinstead of crashing.
- Recall silently dropped memories with unknown confidence (
memanto/app/services/memory_read_service.py)- Memories with
None/missing confidence were filtered out whenevermin_confidence > 0, which primarily hit memories imported viamemanto migrate. Unknown confidence now fails open, matching how expiration filtering handles unparseable dates.OverflowErrorjoinsTypeError/ValueErrorin the parse guard.
- Memories with
- 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.
- An empty query plus filters produced
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_idfrom on-premdata_store.json) on read, so the preservation logic inupdate_memory()never received them. Extra keys are now passed through, excluding known schema keys,memory_type(a duplicate oftype), and the new sharedREMOVED_TRUST_FIELDSfrozenset.- Carry-forward on update now keys off an explicit
_MEMORY_SCHEMA_FIELDSset 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_submittedusedlen(memories)rather thanlen(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 realsession.session_idfor bothrememberandbatch_remember.
- Both clients already resolved a validated session but discarded it and hardcoded
- Malformed batch responses were partially tolerated (
memanto/cli/client/direct_client.py,memanto/cli/client/sdk_client.py)- A missing
resultskey, or a results array whose length doesn't match the submitted records, now raisesMemoryOperationErrorinstead of silently pairing memories withNoneresults.
- A missing
- 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
NamespaceErrorwith the real cause; conflict detection was restructured to catchConflictErroralongside message-based matching, and all failures chain the original exception.
- Moorcheh tier/quota/limit rejections during agent creation now raise a typed
- Renewed session tokens were unreadable by browser clients (
memanto/app/main.py)X-Session-Tokenadded to the CORSexpose_headerslist — custom response headers are not CORS-safelisted, so header-authenticated clients could not read an auto-renewed token.
memanto session infocompared timezones incorrectly (memanto/cli/commands/session.py)- Replaced the deprecated
datetime.utcnow()and invertedtzinfohandling: naive expiry timestamps are now assumed UTC and compared againstdatetime.now(timezone.utc), instead of stripping tzinfo from aware ones.
- Replaced the deprecated
- Negative session durations were accepted (
memanto/app/services/session_service.py)create_session()now rejects non-numeric or negativeduration_hourswith aValueErrorinstead of minting an already-expired session.
- LangGraph store crashed on unexpected
list_agentspayloads (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.contentmax length), keeping the tail — long conversations were previously rejected and dropped silently.
- The store iterated the response directly; a dict payload (
- 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.
- A single category string was iterated per-character.
- 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), notcontainerTags— the wrong parameter silently under-fetched. - Cross-tag deduplication discarded a memory's additional container tags; rows now accumulate a merged
container_tagslist while keeping the singularcontainer_tagper 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, andsource_count()mirrors that logic so the pre-migration summary matches what is actually imported.
- The v4 list endpoint takes
- 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 againstbool(which subclassesint), where YAMLtruepreviously became1970-01-01T00:00:01Zand could expire a durable memory._parse_positive_int()rejects fractional floats rather than truncating them.
- Titles over
- 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
ValueErrorin the CLI and an HTTP 400 in the UI, instead of an unhandledJSONDecodeError/OSError.
- Unreadable or non-JSON export files now produce a clear
- 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") withopenAsBlob()and standardFormData, removing theescapeMultipartValuefilename-escaping workaround.
- Replaced the hand-rolled multipart stream (
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()validatesagent_idagainst^[\w\-]+$anddateagainst^\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 toimported.tests/test_unit.py— file-lock coverage consolidated here, including Windows contention retry without a deadline and non-retry on unexpected errors;original_idsurvives 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 fromtests/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_exportrejects 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: v0.2.16...v0.2.17