Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ about it.
| Tool | Purpose |
|---|---|
| `search_memory` | Relevance search over stored memories (optionally with the user profile) |
| `add_memory` | Store a durable fact or exchange; saves in the background by default (`wait=true` to block until it is searchable) |
| `add_memory` | Store a durable fact or exchange; saves in the background by default (`wait=true` to block until extraction has finished) |
| `get_profile` | The synthesized user profile (facts, traits, preferences) |
| `list_memories` | Chronological, paginated browsing |
| `forget_session` | Delete what this connection stored (memories + cases distilled from its trajectories); the profile and learned skills are kept |
Expand All @@ -107,6 +107,9 @@ permission prompt; `forget_session` is marked destructive.

- Profile updates and case/skill distillation run in an offline pipeline and land seconds
to minutes after the write.
- Extraction finishing and the memory becoming searchable are two steps: memories saved in
the last minute may not be indexed yet, and `list_memories` shows new entries sooner than
`search_memory`.
- A background save that fails is reported on the next tool result, so the model never
silently believes something was remembered.
- Trajectories need more than three tool-call rounds to pass the distillation quality
Expand Down
3 changes: 2 additions & 1 deletion src/everos_mcp/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ def reply(self, text: str) -> str:

async def store(self, messages: list[dict[str, Any]], session_id: str) -> str | None:
"""Write messages, then make sure extraction ran.
Returns the final status; "extracted" means searchable."""
Returns the final status; "extracted" means extraction finished, not that
the memory is searchable yet (the index catches up afterwards)."""
async with self._lock:
return await self._add_and_flush(messages, session_id)

Expand Down
14 changes: 9 additions & 5 deletions src/everos_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,11 @@ async def add_memory(
reports in Chinese"), a habit, a decision, or a long-term goal. Pass what
the user said as user_message and/or your reply as assistant_message.

By default it returns at once and extraction runs in the background;
the memory is searchable a few seconds later, and a failure is reported
on a later tool result. Set wait=true only when you must search for it
right away — it blocks until extraction finishes.
By default it returns at once and extraction runs in the background,
and a failure is reported on a later tool result. Set wait=true to block
until extraction has finished. Either way the search index catches up
afterwards: memories saved in the last minute may not be indexed yet, and
list_memories shows them sooner than search_memory.
"""
conv = _conversation(ctx)
if not user_message and not assistant_message:
Expand Down Expand Up @@ -242,7 +243,10 @@ async def add_memory(
if wait or conv.ephemeral or conv.saturated:
status = await conv.store(messages, s.session_id)
if status == "extracted":
return conv.reply("Stored and extracted; the memory is now searchable.")
return conv.reply(
"Stored and extracted. Memories saved in the last minute may not be "
"indexed yet; list_memories shows them sooner."
)
return conv.reply(
f"Stored, but extraction did not run (status {status!r}). The memory is "
"NOT searchable yet — do not tell the user it was remembered."
Expand Down
9 changes: 6 additions & 3 deletions tests/test_everos_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ def test_add_memory_always_flushes_the_open_tail(tool_client):
}
)
reply = run(server.add_memory(user_message="x", wait=True))
assert "now searchable" in reply
assert "Stored and extracted" in reply

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With only the stable prefix asserted, nothing guards the wording this PR fixes. If "now searchable" comes back later, all three updated assertions still pass. Maybe add assert "now searchable" not in reply here (and at 750 / 841).

assert "now searchable" not in reply
assert paths(seen) == ["add", "flush"]


Expand Down Expand Up @@ -747,7 +748,8 @@ async def scenario():
return saved, forgot

saved, forgot = run(scenario())
assert "now searchable" in saved # waited instead of backgrounding
assert "Stored and extracted" in saved # waited instead of backgrounding
assert "now searchable" not in saved
assert "Nothing to forget" in forgot
assert [r.url.path.rsplit("/", 1)[-1] for r in seen] == ["add", "flush"]

Expand Down Expand Up @@ -838,7 +840,8 @@ def test_saturated_conversation_saves_in_the_foreground(tool_client):
for _ in range(16):
conv._pending.add(object()) # stand-ins for running saves
reply = run(server.add_memory(user_message="x"))
assert "now searchable" in reply
assert "Stored and extracted" in reply
assert "now searchable" not in reply


def test_notices_are_capped():
Expand Down
Loading