Skip to content

feat: v1->v2 migration rules, raw-HTTP support, and scan mode - #3

Open
dani1005 wants to merge 1 commit into
EverMind-AI:mainfrom
dani1005:feat/v1-to-v2-migration
Open

feat: v1->v2 migration rules, raw-HTTP support, and scan mode#3
dani1005 wants to merge 1 commit into
EverMind-AI:mainfrom
dani1005:feat/v1-to-v2-migration

Conversation

@dani1005

@dani1005 dani1005 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

What

Adds the everos-cloud 0.4.x → 1.x (v2 Memory API) migration to the existing v0→v1 skill, and extends the skill to cover callers that speak HTTP directly instead of using the Python SDK.

Why the rules are split in two

File Scope
migration/http/v1-to-v2.md (new, 17 rules) Transport level — endpoints, payloads, responses. Language-agnostic, source of truth
migration/python/v1-to-v2.md (new, 15 rules) Maps the Python SDK surface onto those rules

This migration is fundamentally an API-level change, not just an SDK rename. A customer calling /api/v1/memories from Go, TypeScript or curl hits every one of these breaking changes — and previously got zero help: Step 1 only grepped evermemos|everos_cloud, which never matches a raw caller. Detection now also matches /api/v1/memories, api.evermind.ai and the EverOS env vars.

SKILL.md changes

  • Fixed version detection. It keyed on a client.vN. prefix — but 1.x removed that entirely (client.add(...), not client.v1.memories.add(...)). A migrated repo was misdetected and re-running the skill was not idempotent. Now keys on the dependency constraint plus bare facade verbs.
  • Added --scan mode — produce an impact report, edit nothing. Useful for deciding whether to migrate.
  • Added the blocker list that must always be flagged and never rewritten, plus an impact-report template that leads with those blockers rather than with the mechanical diff count.

Findings that contradict the current public migration guide

Verified against the published 0.4.1 / 1.0.0 / 1.1.0 wheels on PyPI, the v2 OpenAPI contract, and live prod calls (2026-09-04):

  1. 1.x does NOT read EVEROS_API_KEY. api_key is a required argument; there is no os.environ/getenv reference anywhere in client.py. sdk-migration-1x.mdx states it "still reads EVEROS_API_KEY if omitted". This one fails loudly (TypeError), so it is the safe one.
  2. 1.x does NOT read EVER_OS_BASE_URL either — and this fails silently. 0.4.x picked it up from the environment; 1.x only honours host=. A client that pointed at a dev or test gateway via the environment starts reading and writing production data after the upgrade, with no error. Flagged as the top-priority finding in the skill.
  3. Also undocumented: AsyncEverOS is gone (no async client in 1.x at all), as are max_retries / http_client / default_headers0.4.x retried twice by default, 1.x does not retry.

The OpenAPI spec's own AddInput example uses "timestamp": 1700000000 (seconds), which the API rejects with 422 — worth fixing separately in the spec.

Capabilities with no v2 equivalent (flagged, never rewritten)

group memory (/memories/group, /groups, group_id filters), /senders, /settings, AsyncEverOS, delete(memory_id=), and memory_type="raw_message". The v2 contract has zero occurrences of group. These decide whether a given customer's migration can complete at all, so the skill counts them and reports the counts first.

Naming

Marketplace renamed everos-pluginseveros-tools. The GitHub repo rename is a separate manual step (needs admin), and docs.evermind.ai/api-reference/sdk-migration install commands need updating with it.

Testing

  • claude plugin validate . → ✔ passes (same check as CI)
  • examples/python/v2.pypy_compile clean
  • End-to-end on a synthetic v1 customer repo (Python SDK + a TypeScript fetch caller + group usage + seconds timestamps + AsyncEverOS + EVER_OS_BASE_URL pointed at test): detection, scan report, and rewrites all behaved as specified — including catching Math.floor(Date.now()/1000) in the TS file.

⚠️ Not yet run against real customer code. Worth doing before this goes out in any customer-facing email.

🤖 Generated with Claude Code

Adds the everos-cloud 0.4.x -> 1.x (v2 Memory API) migration to the existing
v0->v1 skill, and extends it to cover callers that speak HTTP directly.

Rules are split into two layers:
- migration/http/v1-to-v2.md   transport-level, language-agnostic, source of truth
- migration/python/v1-to-v2.md maps the Python SDK surface onto those rules

Previously only Python SDK users were detected at all: Step 1 grepped for
evermemos|everos_cloud, which never matches a raw caller hitting
api.evermind.ai. Detection now also matches /api/v1/memories and the EverOS env
vars, so a Go/TS/curl caller is covered.

SKILL.md changes:
- Fix version detection. It keyed on a client.vN. prefix, which 1.x removed
  entirely (client.add(...)), so a migrated repo was misdetected and re-running
  the skill was not idempotent. Now keys on the dependency constraint plus bare
  facade verbs.
- Add --scan mode: produce an impact report, edit nothing.
- Add the blocker list that must always be flagged and never rewritten, and an
  impact-report template that leads with those blockers.

Findings verified against the published 0.4.1/1.0.0/1.1.0 wheels, the v2 OpenAPI
contract, and live prod calls (2026-09-04) — two of which contradict the current
public migration guide:
- 1.x does NOT read EVEROS_API_KEY (api_key is a required arg); the guide says
  it still does.
- 1.x does NOT read EVER_OS_BASE_URL either. This one fails silently: a client
  that pointed at dev/test via the environment starts hitting production.
Also undocumented: AsyncEverOS is gone, as are max_retries/http_client/
default_headers (0.4.x retried twice by default, 1.x does not retry).

Marketplace renamed everos-plugins -> everos-tools. The GitHub repo rename is a
separate manual step; docs.evermind.ai links need updating with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@GaussDing

Copy link
Copy Markdown

Validated this PR end-to-end instead of reviewing the rules on paper: built a throwaway v1 caller
(7 modules / 6 test files) against the introspected 0.4.1 surface — deliberately not against this
skill's own examples/python/v1.py, so the skill isn't graded on its own answer key — got it green on
everos-cloud==0.4.1 against the dev gateway (21 passed), installed this PR's plugin, ran
/everos-sdk-upgrade, then re-ran the suite on 1.1.0 with a v2 key.

The mechanical half is solid: version detection landed on v1 (dependency constraint + 29 client.v1.
sites, 0 bare facade verbs — the idempotency fix works), every rewritten signature matches the real
1.1.0 wheel, all 27 blocker call sites were flagged and none rewritten, and all three "contradicts the
published guide" findings reproduce (api_key required positional, no os.environ anywhere in
client.py, AsyncEverOS gone). Final state after the migration: 12 passed / 9 skipped, every skip
a flagged blocker — once the issue in §1.1 was fixed by hand.


1. Defects found

1.1 Task polling has no rule — this one breaks the migrated code (blocking)

The migrated suite raised on the first async write:

AttributeError: 'AddData' object has no attribute 'task_id'

Two independent changes, neither covered by any rule:

(a) The task id is gone from the add response. Verified on the wire:

{"data": {"message_count": 1, "status": "queued"}, "request_id": "0217893676410010..."}

AddData.model_fields == ['message_count', 'status', 'additional_properties']. The skill correctly
applied SDK-011 ("drop one .data level"), so response.data.task_id became response.task_id — valid
Python, AttributeError at runtime. The id to poll is the envelope's top-level request_id;
GET /api/v2/tasks/{request_id} answers 200 and echoes it back as data.id. SDK-011 already says the
facade discards the envelope, but never connects that to task polling.

(b) The status vocabulary changed, and this half fails silently. v2 is queued → pending → success;
v1 was completed. Observed values across GET /api/v2/tasks: only pending and success. A leftover

if response.status in ("completed", "failed", "error"):   # never true on v2

turns a finished task into an apparently-unfinished one and the poll runs to its timeout. Nothing raises.

Related: API-001 labels GET /api/v1/tasks/{task_id}GET /api/v2/tasks/{task_id} as
"Path-only change". It is not — the id source and the status values both changed.

1.2 SDK-014 has the wrong method names — the search patterns miss every real call site

The table lists client.v1.groups.update(...) and client.v1.senders.update(...). In 0.4.x both
resources expose create / retrieve / patch — there is no .update( on either. Only settings
has .update(. A grep built from that table finds nothing.

1.3 SDK-009 aims the agent_memory decision at a call shape that cannot exist

The table lists memory_type="agent_memory" and "raw_message" as get() values. 0.4.1's get
literal is ['episodic_memory', 'profile', 'agent_case', 'agent_skill'] — those two values exist only on
search(memory_types=[...]). So the "needs a human decision" flag points at get, while the place that
actually needs the decision (search) is not covered.

1.4 Two search parameters were removed without being documented

1.1.0's search() has no memory_types= and no include_original_data=; SDK-008's field mapping
lists neither. Consequence for callers: a search can no longer be restricted to a subset of memory
types at all — the response still separates them, so filtering moves client-side.

1.5 Three gaps the model patched at runtime — and it said so in the output

These did not break, because the model worked around them itself. But it left comments naming the
gap, which makes them reproducible findings rather than luck:

Gap What the model did Its own comment
A removed symbol's import (AsyncEverOS) moved it into the function body so import memclient keeps working "the import is deliberately function-local so that import memclient still works and only calling this fails"
1.x validates locally with pydantic accepted (EverOSAPIError, ValueError) in the error tests "which the SDK-012 rule does not cover"
Tests covering a removed capability marked them skip with the migration reason
search(memory_types=) removal (§1.4) dropped it, documented "filter on the way out"

Note the shape of this: the skill left exactly two comments saying the rules do not cover something,
and one of those two spots (task polling, ingest.py) is the one that broke.
The gaps are real and the
model can identify them — which argues for writing them into the rule files rather than re-rolling the
dice each run. It also makes a useful self-check: wherever that phrasing appears in the output, that is
the line a human should review.

Worth calling out that (a) py_compile sees none of §1.1 or the dead-import case, and (b) a stale
top-level import of a removed symbol takes down the entire module — including the paths that migrated
cleanly.


2. Suggested fixes

Ordered by the defect they close. I have these implemented as a patch against this branch
(3 files, +144 / −6, no change to the skill's flow) — happy to push it as a branch if useful.

2.1 → §1.1 Add a task-polling rule to both files

migration/http/v1-to-v2.md — new API-018: Async task polling covering: the add response carries
no task id (with the JSON above); the id is the envelope's request_id; GET /api/v2/tasks/{id} echoes
it as data.id; and a status table completed → success, queued → pending, flagged as a silent
failure. Plus: correct API-001's tasks row from "Path-only change" to "Not path-only — see
API-018", and add a Quick Reference entry under Requires restructuring.

migration/python/v1-to-v2.md — new SDK-016: Task polling with the before/after:

# 0.4.x
response = client.v1.memories.add(user_id=u, session_id=s, messages=msgs, async_mode=True)
task = client.v1.tasks.retrieve(response.data.task_id)
if task.data.status in ("completed", "failed", "error"): ...

# 1.x — the facade drops the envelope, so an async caller that follows its task goes one level down
envelope = client.memory.add_memory(AddInput(
    app_id="default", project_id="default", session_id=s, async_mode=True,
    messages=[MessageItem(sender_id=u, role=m["role"], timestamp=m["timestamp"],
                          content=Content(m["content"])) for m in msgs],
))
task = client.task_get(envelope.request_id)
if task.status in ("success", "failed", "error"): ...
# or: client.task_wait(envelope.request_id, timeout=180, interval=3)

Two notes belong in that rule: task_get returns the unwrapped TaskItem
(id / status / task_type / created_at / finished_at / error) and task_wait replaces the
hand-rolled loop; and the low-level client does not coerce strContent, so MessageItem and
Content must be built explicitly or pydantic rejects the call before it is sent.

Search patterns for the rule: .task_id, tasks.retrieve(, "completed" in a status comparison.

2.2 → §1.2 Fix SDK-014's method names

.update(...).patch(...) on the groups and senders rows, plus a line noting that only settings
has .update( — otherwise a pattern built on update silently matches nothing.

2.3 → §1.3 Move the agent_memory / raw_message rows onto search

Re-label both rows as memory_types=[...] on search, and state that 0.4.x's get never accepted
them (with its actual literal), so the human decision is made at the search call site.

2.4 → §1.4 Document the two removed search parameters

Two rows in SDK-008's field mapping: memory_types=[...](none), "a search can no longer be
restricted to a subset of types; the response still separates them, so filter client-side";
include_original_data=(none).

2.5 → §1.5 Turn the three runtime work-arounds into rules

  • SDK-012, new step 4: EverOSAPIError only covers errors the gateway returned. 1.x validates the
    body with pydantic before sending, and those raise pydantic_core.ValidationError — not an
    EverOSError subclass (it derives from ValueError). 0.4.x surfaced the same input as a server-side
    BadRequestError, so a caller that turned invalid input into its own 4xx now lets the exception
    escape. Suggest except (EverOSAPIError, ValueError) where the caller validates user input.
  • SDK-004 / SDK-013, new step 0: remove the module-level import of the removed symbol first
    ("flag, do not rewrite" applies to the call, not to an import of something that no longer exists);
    move it into the function body so only calling it fails.
  • SKILL.md → "Rules for the migration agent": add the removed-symbol-import rule above, and a line
    on tests covering a removed capability — mark them skip with the migration reason rather than
    deleting them or leaving them red.
  • SKILL.md → "Limitations of syntax checking": add task_id read off an add result; a task status
    compared to "completed"; a leftover import of a removed symbol. And suggest verification run one
    python -c "import <pkg>"py_compile reports success on all three.

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.

2 participants