Skip to content

Reject case-colliding ids when using YAMLStorage#252

Merged
whimo merged 5 commits into
mainfrom
artemy/id-case-collision
Jul 22, 2026
Merged

Reject case-colliding ids when using YAMLStorage#252
whimo merged 5 commits into
mainfrom
artemy/id-case-collision

Conversation

@whimo

@whimo whimo commented Jul 22, 2026

Copy link
Copy Markdown
Member

Closes #249.

Problem

YAMLStorage uses raw logical ids as filenames, so ids differing only by case (X vs x) address the same file on case-insensitive filesystems (macOS / Windows defaults) and silently overwrite each other — datasources, models, and memories alike. Reads and deletes were equally unsafe (get_model("X") returned model x; delete_model("X") removed x.yaml), and the legacy memory migration could collapse colliding records and then delete memories.yaml.

Fix: fail loudly in the YAML backend

Instead of the reversible filename encoding suggested in the issue, saving an id that differs only by case (casefold()-equal, raw-different) from an existing one raises a new IdCollisionError(SlayerError, ValueError) in the YAML backend — deterministically on every platform, so a YAML store created on Linux stays portable to macOS/Windows. Exact-id re-saves remain upserts. SQLite is unchanged: its keys are case-sensitive TEXT PKs and case-variant ids remain distinct rows (a SQLite store holding such a pair can't be exported to the YAML layout without renaming — documented).

Mechanism: the checks live in the StorageBackend base save templates — which preserves the save_model(_validate=False) migration-write-back bypass and coverage through the JoinSyncStorage wrapper — gated on a class flag, _ids_collide_as_filenames (False on the base, True on YAMLStorage, copied from the inner backend by the wrapper). Model saves check both identity components: data_source against registered datasource names ∪ other models' data_sources (they share the models/<ds>/ directory namespace), and name within the same datasource. The datasource check is a public base helper, check_datasource_id_collision(name), called by YAMLStorage.save_datasource; save_datasource itself stays a backend-implemented abstract method, so third-party overrides keep working. A custom backend with a case-insensitive keyspace opts in via the flag + helper.

YAML read/delete exact-name verification

YAML lookups and deletes now compare the exact entry against os.listdir (_exact_entry_exists) instead of os.path.exists/bare open, so on macOS a case-variant get_model/get_datasource/get_memory returns not-found and a delete is a no-op — instead of silently reading or removing the wrong entity in a store copied over from a case-sensitive system.

Migration pre-checks

migrate_memories_layout and migrate_yaml_layout (restructured to plan-then-move) refuse to run when targets would collide by case, before writing anything — legacy files preserved for manual repair.

Surfacing

REST POST /datasources → 400 (model/memory endpoints already mapped ValueError → 400); MCP create_datasource returns a friendly message and its auto-ingest loop reports per-model save failures without aborting (quoted case-variant tables like "Orders"/orders: first wins, second reported); CLI prints-and-exits on datasource create, skips-and-continues in ingest loops. ingest_datasource_idempotent already isolated per-model errors into IngestionResult.errors.

Tests

  • test_case_sensitive_ids (which only passed for the yaml param because CI runs on a case-sensitive FS) becomes test_case_variant_ids: YAML rejects, SQLite stores both distinctly.
  • New tests/test_id_collision.py: helpers; YAML rejection for all three entity kinds incl. cross-namespace datasource/model checks and the _validate=False bypass; SQLite-allows coverage (direct and wrapped); the resolve_storage (JoinSync-wrapped YAML) path; migration refusals.
  • New tests/test_yaml_exact_entry.py: read/delete not-found semantics for case-variant lookups (assertions hold on both case-sensitive and case-insensitive dev machines).
  • REST 400 test. Full suite: 6838 passed. The issue's repro raises on YAML with originals intact (verified on case-insensitive APFS).

Known scope choice: the key is casefold() only — slightly broader than ASCII case (ßss); Unicode-normalization aliasing (NFC vs NFD) is deliberately out of scope.

https://claude.ai/code/session_011sbQKNXcUtGAsCXtmJXHvR

Summary by CodeRabbit

  • New Features
    • Default YAML storage now rejects datasource, model, and memory IDs that differ only by letter case, raising IdCollisionError with collision details.
  • Bug Fixes
    • YAML reads/updates/deletes now treat case-variant entries as “not found” and prevent layout migration from making filesystem changes when collisions are detected.
    • Ingestion and auto-ingestion now continue after per-model/per-item save failures (best-effort), with failure details reported.
  • User Experience
    • REST datasource creation returns HTTP 400 on case conflicts; CLI and MCP report failed models without stopping the whole run.
  • Documentation
    • Updated storage, models, and memories docs to clarify case-collision rules, exact-case behavior, and custom backend guidance.

YAMLStorage uses raw ids as filenames, so ids differing only by case
(X vs x) address the same file on case-insensitive filesystems (macOS,
Windows) and silently overwrite each other — datasources, models, and
memories alike. SQLiteStorage kept them distinct, so the backends
silently diverged.

Saving an id that differs only by case from an existing one now raises
IdCollisionError(SlayerError, ValueError), enforced in the
StorageBackend base templates so every backend rejects on every
platform and stores stay portable. Exact-id re-saves remain upserts; a
legacy store already holding a colliding pair raises on a save of
either spelling.

- save_datasource becomes a template method delegating to a new
  abstract _save_datasource_impl (same pattern as save_model)
- save_memory checks via a new cheap _list_memory_ids() primitive;
  SQLiteStorage's atomic save_memory override duplicates the check
  inside its transaction
- model saves check both identity components: data_source against
  datasource names and other models' data_sources, name within the
  same datasource
- YAML reads/deletes verify the exact directory entry via os.listdir
  comparison, so a case-variant lookup reads as not-found and a delete
  is a no-op instead of hitting the wrong file
- migrate_memories_layout and migrate_yaml_layout pre-check collisions
  before writing anything, preserving legacy files on failure
- surfacing: REST POST /datasources maps to 400; MCP create_datasource
  returns a friendly message and isolates per-model ingest-save
  failures; CLI prints and exits or skips-and-continues in ingest loops

Closes #249

Claude-Session: https://claude.ai/code/session_011sbQKNXcUtGAsCXtmJXHvR
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

YAML storage now rejects case-only identifier collisions for datasources, models, and memories, enforces exact-case lookups, prechecks migrations, and surfaces failures through REST, CLI, and MCP flows. SQLite retains distinct case-variant identifiers. Tests and documentation cover the updated behavior.

Changes

Case-collision persistence safeguards

Layer / File(s) Summary
Collision contracts and shared validation
slayer/core/errors.py, slayer/storage/base.py, slayer/storage/join_sync.py
Adds IdCollisionError, shared casefold-based checks, and filename-collision behavior propagation through wrapped storage.
Exact YAML access and migration prechecks
slayer/storage/yaml_storage.py, slayer/storage/v4_migration.py
Adds exact-case entry checks and validates migration destinations before writing.
REST, CLI, and MCP error handling
slayer/api/server.py, slayer/cli.py, slayer/mcp/server.py, tests/test_api_server.py
Maps persistence failures to interface responses and continues ingestion after per-model save failures.
Collision and exact-entry regression coverage
tests/test_id_collision.py, tests/test_yaml_exact_entry.py, tests/test_memory_string_ids.py
Tests YAML rejection, SQLite coexistence, migration safety, exact-case access, wrapper propagation, memory behavior, and API handling.
Persistence contract documentation
CLAUDE.md, docs/concepts/*, docs/configuration/storage.md
Documents collision, lookup, migration, backend-hook, and error-surfacing behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Interface
  participant StorageBackend
  participant YAMLStorage
  Client->>Interface: create datasource or ingest models
  Interface->>StorageBackend: save entity
  StorageBackend->>YAMLStorage: validate case-folded identity
  YAMLStorage-->>StorageBackend: raise IdCollisionError
  StorageBackend-->>Interface: return ValueError-compatible error
  Interface-->>Client: HTTP 400 or per-model failure
Loading

Possibly related PRs

  • MotleyAI/slayer#251: Both changes address YAML identity collisions on case-insensitive filesystems.

Suggested reviewers: aivanf, zmeigorynych

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.20% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses #249 by rejecting casefold-equal YAML IDs, preserving SQLite behavior, and fixing reads, deletes, and migration collision handling.
Out of Scope Changes check ✅ Passed The code changes stay focused on YAML collision handling, error surfacing, migrations, and tests, with no obvious unrelated additions.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main YAMLStorage case-collision rejection change.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch artemy/id-case-collision

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
slayer/storage/join_sync.py (1)

259-282: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Delegate save_memory to the inner backend
JoinSyncStorage still needs a public save_memory override that forwards to self._inner.save_memory(...). The inherited template splits the collision check from the write and bypasses SQLiteStorage.save_memory’s transaction, even though the low-level SQLite helpers keep memory_entities in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/join_sync.py` around lines 259 - 282, Add a public save_memory
override to JoinSyncStorage that forwards the memory and any supported arguments
directly to self._inner.save_memory(...). Place it alongside the existing memory
delegation methods, ensuring calls use SQLiteStorage.save_memory’s transactional
implementation instead of the inherited template.
🧹 Nitpick comments (3)
slayer/storage/sqlite_storage.py (1)

439-452: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate "SELECT id FROM memories" literal (SonarCloud S1192).

The same literal appears 3 times (here, _next_memory_seq_sync_from_conn, _list_memory_ids_sync). Extract a module-level constant to avoid drift between the three query sites.

♻️ Proposed fix
+_SELECT_MEMORY_IDS_SQL = "SELECT id FROM memories"
+
 ...
-                    ids = [
-                        row[0] if isinstance(row[0], str) else str(row[0])
-                        for row in conn.execute("SELECT id FROM memories")
-                    ]
+                    ids = [
+                        row[0] if isinstance(row[0], str) else str(row[0])
+                        for row in conn.execute(_SELECT_MEMORY_IDS_SQL)
+                    ]

Also applies to: 494-505, 562-569

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/sqlite_storage.py` around lines 439 - 452, Extract the
repeated "SELECT id FROM memories" SQL into a module-level constant, then update
the query in the transaction case-collision check and the corresponding queries
in _next_memory_seq_sync_from_conn and _list_memory_ids_sync to use that
constant.

Source: Linters/SAST tools

slayer/storage/base.py (1)

126-141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_find_case_colliding_id is called positionally everywhere; guideline requires keyword args for multi-param functions.

Every call site (_check_model_identity_collision lines 236/242, save_datasource line 465, save_memory line 702, and sqlite_storage.py line 446) passes candidate/existing positionally. As per coding guidelines, "Use keyword arguments for functions with more than one parameter." Consider marking the params keyword-only (*) to enforce this at every call site.

♻️ Proposed fix
 def _find_case_colliding_id(
-    candidate: str, existing: Iterable[str],
+    *, candidate: str, existing: Iterable[str],
 ) -> str | None:

Then update each call site to _find_case_colliding_id(candidate=..., existing=...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/base.py` around lines 126 - 141, Make _find_case_colliding_id
parameters keyword-only by adding the separator before candidate, then update
every call in _check_model_identity_collision, save_datasource, save_memory, and
sqlite_storage.py to pass candidate= and existing= explicitly. Preserve the
function’s collision behavior unchanged.

Source: Path instructions

docs/configuration/storage.md (1)

85-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: "enforced in the shared base class" is imprecise for the SQLite memory path.

For SQLite, memory-id collision checking is actually reimplemented locally in _save_memory_atomic_sync (for transactional atomicity), not routed through the shared base-class check used by models/datasources. Worth a one-clause caveat if you want full precision, but this doesn't materially mislead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/configuration/storage.md` around lines 85 - 86, Update the SQLite
case-sensitivity documentation to clarify that collision enforcement uses the
shared base class for models and datasources, while the SQLite memory path
reimplements the check locally in _save_memory_atomic_sync for transactional
atomicity. Preserve the existing export and cross-platform migration guidance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@slayer/storage/base.py`:
- Around line 453-470: Make the collision check and persistence in
save_datasource and save_model atomic for SQLite by executing both under the
backend’s existing BEGIN IMMEDIATE transaction pattern. Keep the list/identity
checks and corresponding _save_datasource_impl or model-save implementation
within the same transaction, so concurrent case-variant saves cannot both pass
validation; preserve the existing memory-path behavior.

---

Outside diff comments:
In `@slayer/storage/join_sync.py`:
- Around line 259-282: Add a public save_memory override to JoinSyncStorage that
forwards the memory and any supported arguments directly to
self._inner.save_memory(...). Place it alongside the existing memory delegation
methods, ensuring calls use SQLiteStorage.save_memory’s transactional
implementation instead of the inherited template.

---

Nitpick comments:
In `@docs/configuration/storage.md`:
- Around line 85-86: Update the SQLite case-sensitivity documentation to clarify
that collision enforcement uses the shared base class for models and
datasources, while the SQLite memory path reimplements the check locally in
_save_memory_atomic_sync for transactional atomicity. Preserve the existing
export and cross-platform migration guidance.

In `@slayer/storage/base.py`:
- Around line 126-141: Make _find_case_colliding_id parameters keyword-only by
adding the separator before candidate, then update every call in
_check_model_identity_collision, save_datasource, save_memory, and
sqlite_storage.py to pass candidate= and existing= explicitly. Preserve the
function’s collision behavior unchanged.

In `@slayer/storage/sqlite_storage.py`:
- Around line 439-452: Extract the repeated "SELECT id FROM memories" SQL into a
module-level constant, then update the query in the transaction case-collision
check and the corresponding queries in _next_memory_seq_sync_from_conn and
_list_memory_ids_sync to use that constant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0af37df7-0ea6-446f-9710-556c7866a3a0

📥 Commits

Reviewing files that changed from the base of the PR and between cdeadff and 5c50e6b.

📒 Files selected for processing (18)
  • CLAUDE.md
  • docs/concepts/ingestion.md
  • docs/concepts/memories.md
  • docs/concepts/models.md
  • docs/configuration/storage.md
  • slayer/api/server.py
  • slayer/cli.py
  • slayer/core/errors.py
  • slayer/mcp/server.py
  • slayer/storage/base.py
  • slayer/storage/join_sync.py
  • slayer/storage/sqlite_storage.py
  • slayer/storage/v4_migration.py
  • slayer/storage/yaml_storage.py
  • tests/test_api_server.py
  • tests/test_id_collision.py
  • tests/test_memory_string_ids.py
  • tests/test_yaml_exact_entry.py

Comment thread slayer/storage/base.py Outdated
Comment on lines +453 to +470
async def save_datasource(self, datasource: DatasourceConfig) -> None:
"""Persist a datasource config.

Rejects a name that differs only by case from an existing
datasource name — or from a saved model's ``data_source`` (they
share the ``models/<ds>/`` directory namespace in the YAML
backend) — then delegates to the backend-specific
:meth:`_save_datasource_impl`. Same-name saves are upserts.
Concrete backends must NOT override this method.
"""
existing = set(await self.list_datasources())
existing.update(ds for ds, _ in await self._list_all_model_identities())
collide = _find_case_colliding_id(datasource.name, existing)
if collide is not None:
raise IdCollisionError(
kind="datasource", new_id=datasource.name, existing_id=collide,
)
await self._save_datasource_impl(datasource)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Does asyncio.to_thread yield control to the event loop, allowing interleaving between two separate to_thread calls?

💡 Result:

Yes, asyncio.to_thread yields control to the event loop, allowing for interleaving [1][2]. When you call await asyncio.to_thread(...), the current coroutine suspends and yields control back to the event loop [1]. While the function submitted to to_thread is executing in a separate thread (via a ThreadPoolExecutor), the event loop remains free to run other tasks, callbacks, or perform I/O operations [1][2]. Regarding the interleaving of two separate to_thread calls: 1. If you await each to_thread call sequentially (e.g., await asyncio.to_thread(func1); await asyncio.to_thread(func2)), the second call will not start until the first has finished because the event loop is blocked at the first await expression [3]. 2. If you want two separate to_thread calls to run concurrently and interleave, you must schedule them as concurrent tasks, such as by using asyncio.gather or by creating tasks with asyncio.create_task [3]. This allows the event loop to manage both operations simultaneously while waiting for their completion in separate threads [3]. In essence, asyncio.to_thread is a non-blocking awaitable that allows you to offload blocking synchronous code to a separate thread without stalling the event loop, provided that you properly schedule the calls to run concurrently [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== base.py outline ==\n'
ast-grep outline slayer/storage/base.py --view expanded | sed -n '1,220p'

printf '\n== sqlite_storage.py outline ==\n'
ast-grep outline slayer/storage/sqlite_storage.py --view expanded | sed -n '1,260p'

printf '\n== relevant save/collision methods ==\n'
rg -n "async def (save_datasource|save_model|_save_datasource_impl|_save_model_impl|_list_all_model_identities|list_datasources|_check_model_identity_collision|_save_memory_atomic_sync|BEGIN IMMEDIATE)" slayer/storage/base.py slayer/storage/sqlite_storage.py

Repository: MotleyAI/slayer

Length of output: 7188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for p in [Path('slayer/storage/base.py'), Path('slayer/storage/sqlite_storage.py')]:
    print(f"\n### {p} ###")
    text = p.read_text()
    for needle in [
        'async def save_datasource',
        'async def save_model',
        'async def _save_datasource_impl',
        'async def _save_model_impl',
        'async def _list_all_model_identities',
        'async def list_datasources',
        'async def _check_model_identity_collision',
        'BEGIN IMMEDIATE',
        'asyncio.to_thread',
        'asyncio.Lock',
    ]:
        if needle in text:
            print(f"FOUND {needle}")
PY

Repository: MotleyAI/slayer

Length of output: 674


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' slayer/storage/base.py | nl -ba | sed -n '1,260p'
printf '\n----\n'
sed -n '1,260p' slayer/storage/sqlite_storage.py | nl -ba | sed -n '1,260p'

Repository: MotleyAI/slayer

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== base.py relevant excerpts ==\n'
sed -n '200,280p' slayer/storage/base.py
printf '\n----\n'
sed -n '453,490p' slayer/storage/base.py
printf '\n====\n'

printf '\n== sqlite_storage.py relevant excerpts ==\n'
sed -n '186,380p' slayer/storage/sqlite_storage.py
printf '\n----\n'
sed -n '404,540p' slayer/storage/sqlite_storage.py
printf '\n----\n'
sed -n '540,640p' slayer/storage/sqlite_storage.py

Repository: MotleyAI/slayer

Length of output: 22447


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== sqlite_storage init/schema excerpts ==\n'
sed -n '133,185p' slayer/storage/sqlite_storage.py

printf '\n== schema-related searches ==\n'
rg -n "CREATE TABLE|CREATE UNIQUE|UNIQUE INDEX|COLLATE NOCASE|PRIMARY KEY|models \\(|datasources \\(" slayer/storage/sqlite_storage.py slayer/storage/*.py

Repository: MotleyAI/slayer

Length of output: 5760


Make the case-collision guard atomic on SQLite
save_model and save_datasource can still race across the asyncio.to_thread boundaries: SQLite’s PRIMARY KEY here is case-sensitive, so concurrent saves like Foo/foo can both pass the check and persist. The memory path already uses BEGIN IMMEDIATE; models/datasources need the same protection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/base.py` around lines 453 - 470, Make the collision check and
persistence in save_datasource and save_model atomic for SQLite by executing
both under the backend’s existing BEGIN IMMEDIATE transaction pattern. Keep the
list/identity checks and corresponding _save_datasource_impl or model-save
implementation within the same transaction, so concurrent case-variant saves
cannot both pass validation; preserve the existing memory-path behavior.

…c helper

The template-method split (save_datasource delegating to a new abstract
_save_datasource_impl) forced third-party backends to rename their
overrides on upgrade. Revert it: save_datasource stays the abstract
method backends implement directly, and the case-collision check moves
into a public base helper, check_datasource_id_collision(name), which
the built-in YAML/SQLite implementations call before writing. Custom
backends opt into the same protection by calling it from their own
save_datasource.

Claude-Session: https://claude.ai/code/session_011sbQKNXcUtGAsCXtmJXHvR

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
slayer/storage/yaml_storage.py (2)

108-119: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle unreadable directories consistently with the helper contract.

The docstring says missing or unreadable directories return False, but PermissionError from os.listdir() is not caught. Either catch PermissionError or update the contract and tests to propagate permission failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/yaml_storage.py` around lines 108 - 119, Update
_exact_entry_exists to catch PermissionError from os.listdir and return False,
preserving the helper’s documented behavior for missing, invalid, and unreadable
directories.

201-220: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject collisions among already-existing memory files.

The id_by_key construction silently overwrites an earlier stem when two existing files differ only by case. With both X.md and x.md present, migration behavior depends on os.listdir() order and may allow a later write to clobber data. Detect duplicate casefold keys while indexing existing files and fail before processing or writing legacy rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/yaml_storage.py` around lines 201 - 220, Update the
existing-file indexing in the id_by_key construction to detect when two .md
stems share the same casefold key but differ in original casing, and raise the
same collision ValueError immediately. Do this while iterating
os.listdir(mem_dir), before processing normalized legacy rows or performing any
writes; retain the existing normalized-row collision checks.
slayer/storage/base.py (2)

236-236: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use keyword arguments for the collision helper.

These _find_case_colliding_id calls pass multiple parameters positionally. As per coding guidelines, Python functions with more than one parameter must use keyword arguments; update all four call sites using the helper’s declared parameter names.

Also applies to: 242-242, 474-474, 705-705

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/base.py` at line 236, Update all four call sites of
_find_case_colliding_id in the affected storage flows to pass every argument by
keyword, using the helper’s declared parameter names. Preserve the existing
argument values and behavior while removing positional argument usage.

Source: Coding guidelines


217-221: ⚠️ Potential issue | 🟠 Major

Keep collision validation and persistence atomic.

save_model performs the collision check and backend write in separate awaits, while check_datasource_id_collision is also only a preflight helper. The supplied SQLite and YAML implementations still check first and write afterward, so concurrent Foo/foo saves can both pass validation. Keep each backend’s check-and-write sequence under the same transaction or lock.

Also applies to: 462-478

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/base.py` around lines 217 - 221, Make the collision validation
and persistence atomic in save_model and the SQLite/YAML backend
implementations: execute _check_model_identity_collision (or
check_datasource_id_collision) and the corresponding _save_model_impl write
within the same transaction or lock. Ensure concurrent Foo/foo saves cannot both
pass validation before either write commits, while preserving the existing
validation and column-cycle checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@slayer/storage/sqlite_storage.py`:
- Around line 352-354: Make datasource collision validation atomic with
persistence: in slayer/storage/sqlite_storage.py lines 352-354, move
check_datasource_id_collision into the same SQLite write transaction used by
_save_datasource_sync; in slayer/storage/yaml_storage.py lines 410-415,
serialize collision validation and file creation using the datasource lock or
equivalent atomic operation. Preserve case-insensitive collision handling for
concurrent saves.

---

Outside diff comments:
In `@slayer/storage/base.py`:
- Line 236: Update all four call sites of _find_case_colliding_id in the
affected storage flows to pass every argument by keyword, using the helper’s
declared parameter names. Preserve the existing argument values and behavior
while removing positional argument usage.
- Around line 217-221: Make the collision validation and persistence atomic in
save_model and the SQLite/YAML backend implementations: execute
_check_model_identity_collision (or check_datasource_id_collision) and the
corresponding _save_model_impl write within the same transaction or lock. Ensure
concurrent Foo/foo saves cannot both pass validation before either write
commits, while preserving the existing validation and column-cycle checks.

In `@slayer/storage/yaml_storage.py`:
- Around line 108-119: Update _exact_entry_exists to catch PermissionError from
os.listdir and return False, preserving the helper’s documented behavior for
missing, invalid, and unreadable directories.
- Around line 201-220: Update the existing-file indexing in the id_by_key
construction to detect when two .md stems share the same casefold key but differ
in original casing, and raise the same collision ValueError immediately. Do this
while iterating os.listdir(mem_dir), before processing normalized legacy rows or
performing any writes; retain the existing normalized-row collision checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 911612e1-ee36-4025-a8b3-b3e2b6b8e0b5

📥 Commits

Reviewing files that changed from the base of the PR and between 5c50e6b and e17e7db.

📒 Files selected for processing (6)
  • CLAUDE.md
  • docs/configuration/storage.md
  • slayer/storage/base.py
  • slayer/storage/join_sync.py
  • slayer/storage/sqlite_storage.py
  • slayer/storage/yaml_storage.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/configuration/storage.md
  • CLAUDE.md

Comment on lines 352 to 354
async def save_datasource(self, datasource: DatasourceConfig) -> None:
await self.check_datasource_id_collision(datasource.name)
await asyncio.to_thread(self._save_datasource_sync, datasource)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Datasource collision checks are not atomic with persistence.

Both backends can accept concurrent case-variant saves because validation completes before the write begins.

  • slayer/storage/sqlite_storage.py#L352-L354: perform collision validation inside the same SQLite write transaction as the upsert.
  • slayer/storage/yaml_storage.py#L410-L415: serialize collision validation and file creation with a datasource lock or equivalent atomic operation.
📍 Affects 2 files
  • slayer/storage/sqlite_storage.py#L352-L354 (this comment)
  • slayer/storage/yaml_storage.py#L410-L415
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/sqlite_storage.py` around lines 352 - 354, Make datasource
collision validation atomic with persistence: in
slayer/storage/sqlite_storage.py lines 352-354, move
check_datasource_id_collision into the same SQLite write transaction used by
_save_datasource_sync; in slayer/storage/yaml_storage.py lines 410-415,
serialize collision validation and file creation using the datasource lock or
equivalent atomic operation. Preserve case-insensitive collision handling for
concurrent saves.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
slayer/storage/yaml_storage.py (3)

197-213: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject collisions already present on disk.

The existing-stem index overwrites a prior entry with the same casefold() key. If both Foo.md and foo.md already exist, migration proceeds and deletes memories.yaml instead of failing before data changes. Detect an existing prior collision while building the index and raise before any writes or removal.

Proposed fix
         for fname in os.listdir(mem_dir):
             if fname.endswith(".md"):
                 stem = fname[: -len(".md")]
+                prior = id_by_key.get(stem.casefold())
+                if prior is not None and prior != stem:
+                    raise ValueError(
+                        f"{base_dir}: memory ids {prior!r} and {stem!r} "
+                        "differ only by case. Rename one, then reopen."
+                    )
                 id_by_key[stem.casefold()] = stem
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/yaml_storage.py` around lines 197 - 213, Update the
existing-stem index construction in the migration flow to detect when a new .md
filename’s casefolded key already maps to a different stem, rather than
overwriting id_by_key. Raise the same collision ValueError during directory
scanning, before processing normalized records or performing any writes or
memories.yaml removal; retain the current normalized-ID collision checks.

297-304: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use keyword arguments for the new multi-parameter helper calls.

The added calls to _exact_entry_exists and _model_entry_exists pass multiple arguments positionally, contrary to the repository’s Python guideline. Use explicit keywords at these call sites.

As per coding guidelines, use keyword arguments for functions with more than one parameter.

Also applies to: 337-338, 357-359, 374-378, 412-413, 436-439, 574-575, 605-606

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/yaml_storage.py` around lines 297 - 304, Update every
multi-parameter call to _exact_entry_exists and _model_entry_exists in the
affected storage methods to pass arguments by explicit keyword, including
_model_entry_exists itself and the additional call sites noted in the review.
Preserve the existing argument values and behavior while following the
repository’s keyword-argument convention.

Source: Coding guidelines


401-406: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make datasource save/check atomic. check_datasource_id_collision() still races with the write here, so two concurrent saves can both pass and then alias/overwrite on a case-insensitive filesystem. Hold the check and persistence under the same lock, or use an atomic reservation path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/yaml_storage.py` around lines 401 - 406, Update
save_datasource to make the collision check and YAML persistence one atomic
operation: acquire the same async lock around check_datasource_id_collision,
path construction, serialization, and file write, ensuring concurrent saves
cannot both pass the check and overwrite or alias the datasource file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@slayer/storage/yaml_storage.py`:
- Around line 197-213: Update the existing-stem index construction in the
migration flow to detect when a new .md filename’s casefolded key already maps
to a different stem, rather than overwriting id_by_key. Raise the same collision
ValueError during directory scanning, before processing normalized records or
performing any writes or memories.yaml removal; retain the current normalized-ID
collision checks.
- Around line 297-304: Update every multi-parameter call to _exact_entry_exists
and _model_entry_exists in the affected storage methods to pass arguments by
explicit keyword, including _model_entry_exists itself and the additional call
sites noted in the review. Preserve the existing argument values and behavior
while following the repository’s keyword-argument convention.
- Around line 401-406: Update save_datasource to make the collision check and
YAML persistence one atomic operation: acquire the same async lock around
check_datasource_id_collision, path construction, serialization, and file write,
ensuring concurrent saves cannot both pass the check and overwrite or alias the
datasource file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cea1a277-50b6-4a93-9a41-0b8b999e905b

📥 Commits

Reviewing files that changed from the base of the PR and between e17e7db and 1fd0353.

📒 Files selected for processing (10)
  • slayer/core/errors.py
  • slayer/mcp/server.py
  • slayer/storage/base.py
  • slayer/storage/join_sync.py
  • slayer/storage/sqlite_storage.py
  • slayer/storage/v4_migration.py
  • slayer/storage/yaml_storage.py
  • tests/test_id_collision.py
  • tests/test_memory_string_ids.py
  • tests/test_yaml_exact_entry.py
💤 Files with no reviewable changes (2)
  • tests/test_memory_string_ids.py
  • slayer/storage/join_sync.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • slayer/core/errors.py
  • slayer/mcp/server.py
  • slayer/storage/sqlite_storage.py
  • tests/test_yaml_exact_entry.py
  • slayer/storage/v4_migration.py
  • slayer/storage/base.py
  • tests/test_id_collision.py

SQLite keys are case-sensitive TEXT PKs and handle case-variant ids
fine; enforcing the rule there added complexity (a duplicated
transaction check, the _list_memory_ids primitive with per-backend
overrides) for no real protection. Revert SQLite to its original
behavior: case-variant ids are distinct rows.

The checks stay in the base save templates — preserving the
_validate=False migration bypass and coverage through JoinSyncStorage —
gated on a class flag, _ids_collide_as_filenames (True on YAMLStorage,
copied from the inner backend by the wrapper). Custom backends with a
case-insensitive keyspace can opt in via the same flag plus
check_datasource_id_collision.

Claude-Session: https://claude.ai/code/session_011sbQKNXcUtGAsCXtmJXHvR

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/configuration/storage.md (1)

114-131: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the custom-backend example asynchronous.

The StorageBackend methods are async, but the example declares them with def; Line 131 then requires await. A copied backend will not satisfy the runtime contract and will fail when the base template awaits its implementation.

As per coding guidelines, storage operations are async-first.

Suggested documentation fix
-    def _save_model_impl(self, model: SlayerModel) -> None: ...
-    def _list_all_model_identities(self) -> list[tuple[str, str]]: ...
-    def get_model(self, name: str, data_source: str | None = None) -> SlayerModel | None: ...
-    def _delete_model_row(self, *, data_source: str, name: str) -> bool: ...
+    async def _save_model_impl(self, model: SlayerModel) -> None: ...
+    async def _list_all_model_identities(self) -> list[tuple[str, str]]: ...
+    async def get_model(self, name: str, data_source: str | None = None) -> SlayerModel | None: ...
+    async def _delete_model_row(self, *, data_source: str, name: str) -> bool: ...

-    def save_datasource(self, datasource: DatasourceConfig) -> None: ...
-    def get_datasource(self, name: str) -> DatasourceConfig | None: ...
-    def list_datasources(self) -> list[str]: ...
-    def _delete_datasource_row(self, name: str) -> bool: ...
+    async def save_datasource(self, datasource: DatasourceConfig) -> None: ...
+    async def get_datasource(self, name: str) -> DatasourceConfig | None: ...
+    async def list_datasources(self) -> list[str]: ...
+    async def _delete_datasource_row(self, name: str) -> bool: ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/configuration/storage.md` around lines 114 - 131, Update the
custom-backend example methods to use async definitions consistent with the
StorageBackend contract, including _save_model_impl, _list_all_model_identities,
get_model, _delete_model_row, save_datasource, get_datasource, list_datasources,
and _delete_datasource_row. Preserve the existing method signatures and ensure
the save_datasource example can await check_datasource_id_collision.

Source: Coding guidelines

🧹 Nitpick comments (1)
slayer/storage/base.py (1)

233-239: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use keyword arguments for the collision helper.

_find_case_colliding_id receives more than one argument positionally in both changed paths. Change these calls to candidate=... and existing=....

As per coding guidelines, use keyword arguments for functions with more than one parameter.

Also applies to: 690-695

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@slayer/storage/base.py` around lines 233 - 239, Update both
_find_case_colliding_id calls in the affected collision-check paths to pass the
candidate and existing collections using the keyword arguments candidate= and
existing=. Preserve the current values and collision handling behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_id_collision.py`:
- Around line 3-6: Update tests/test_id_collision.py lines 3-6 to document
uniform case-insensitive collision rejection across backends; update lines
191-220 so both direct SQLite and resolve_storage() saves expect
IdCollisionError for case-variant IDs while preserving exact-ID upserts; update
tests/test_memory_string_ids.py lines 183-198 to remove the YAML-only branch and
assert collision rejection for every backend.

---

Outside diff comments:
In `@docs/configuration/storage.md`:
- Around line 114-131: Update the custom-backend example methods to use async
definitions consistent with the StorageBackend contract, including
_save_model_impl, _list_all_model_identities, get_model, _delete_model_row,
save_datasource, get_datasource, list_datasources, and _delete_datasource_row.
Preserve the existing method signatures and ensure the save_datasource example
can await check_datasource_id_collision.

---

Nitpick comments:
In `@slayer/storage/base.py`:
- Around line 233-239: Update both _find_case_colliding_id calls in the affected
collision-check paths to pass the candidate and existing collections using the
keyword arguments candidate= and existing=. Preserve the current values and
collision handling behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f718a0fc-cbcf-4a6b-927f-a22b8f241fff

📥 Commits

Reviewing files that changed from the base of the PR and between 1fd0353 and 4bf77dc.

📒 Files selected for processing (11)
  • CLAUDE.md
  • docs/concepts/ingestion.md
  • docs/concepts/memories.md
  • docs/concepts/models.md
  • docs/configuration/storage.md
  • slayer/core/errors.py
  • slayer/storage/base.py
  • slayer/storage/join_sync.py
  • slayer/storage/yaml_storage.py
  • tests/test_id_collision.py
  • tests/test_memory_string_ids.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/concepts/models.md
  • docs/concepts/ingestion.md
  • slayer/core/errors.py
  • CLAUDE.md
  • slayer/storage/yaml_storage.py

Comment on lines +3 to +6
Ids are filenames in the YAML backend, so saving an id that differs only
by case from an existing one raises IdCollisionError there (exact-id
re-saves remain upserts). SQLite keys are case-sensitive and stores
case-variant ids distinctly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Align regression expectations with the cross-backend collision contract.

These tests codify SQLite-specific acceptance of casefold-colliding IDs, contrary to the PR objective that collisions raise IdCollisionError across storage backends.

  • tests/test_id_collision.py#L3-L6: document uniform collision rejection rather than SQLite-specific distinct identities.
  • tests/test_id_collision.py#L191-L220: expect IdCollisionError for SQLite direct and resolve_storage() saves.
  • tests/test_memory_string_ids.py#L183-L198: remove the YAML-only branch and assert collision rejection for every backend.
📍 Affects 2 files
  • tests/test_id_collision.py#L3-L6 (this comment)
  • tests/test_id_collision.py#L191-L220
  • tests/test_memory_string_ids.py#L183-L198
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_id_collision.py` around lines 3 - 6, Update
tests/test_id_collision.py lines 3-6 to document uniform case-insensitive
collision rejection across backends; update lines 191-220 so both direct SQLite
and resolve_storage() saves expect IdCollisionError for case-variant IDs while
preserving exact-ID upserts; update tests/test_memory_string_ids.py lines
183-198 to remove the YAML-only branch and assert collision rejection for every
backend.

- migrate_memories_layout now also rejects a case-colliding pair already
  present among memories/ stems (possible on case-sensitive filesystems),
  instead of letting listdir order decide which spelling wins the index
- docs custom-backend example methods are async, matching the contract
- multi-parameter helper calls use keyword arguments per convention

Claude-Session: https://claude.ai/code/session_011sbQKNXcUtGAsCXtmJXHvR
@whimo

whimo commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

Review triage (@coderabbitai), addressed in 8ad935f:

Fixed

  • migrate_memories_layout now rejects a case-colliding pair already present among memories/ stems (previously the index silently kept whichever spelling os.listdir returned last); pinned by a new test using a patched listdir so it's deterministic on case-insensitive dev machines too.
  • Custom-backend example in docs/configuration/storage.md uses async def, matching the StorageBackend contract.
  • Multi-parameter helper calls (_find_case_colliding_id, _exact_entry_exists, _model_entry_exists, _check_layout_case_collisions) use keyword arguments per repo convention.

Skipped

  • SQLite atomicity of collision checks — stale: SQLite no longer performs collision checks at all. The rule was deliberately scoped to the YAML backend (ids-as-filenames) in 4bf77dc; SQLite's case-sensitive PKs handle case variants correctly.
  • Tests contradict the PR objective (uniform rejection) — the objective changed: YAML-only rejection is the final contract (see updated PR description); the tests pin it deliberately, including SQLite's distinct-identity behavior.
  • Serialize YAML datasource check+write under a lock — the lock-free last-writer-wins exposure on YAML config writes predates this PR and is not worsened by it; adding lock infrastructure for that pre-existing race is out of scope.
  • Catch PermissionError in _exact_entry_exists — propagating a permissions failure loudly is preferable to reporting "not found" for an unreadable storage dir; the docstring makes no contrary claim.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Relevant files ==='
fd -a -t f -E .git '^(yaml_storage|base|v4_migration|storage)\.py$|^test_id_collision\.py$|^storage\.md$' . \
  | sed -n '1,160p'

printf '%s\n' '=== YAML migration and collision-related symbols ==='
ast-grep outline slayer/storage/yaml_storage.py --items all --match 'migrate_memories_layout|_find_case_colliding_id|_exact_entry_exists|_model_entry_exists' --view expanded || true
ast-grep outline slayer/storage/v4_migration.py --items all --match '_check_layout_case_collisions|migrate_yaml_layout' --view expanded || true

printf '%s\n' '=== Implementation excerpts ==='
rg -n -A55 -B12 'def migrate_memories_layout|def _exact_entry_exists|def _model_entry_exists|_find_case_colliding_id\(' slayer/storage/yaml_storage.py
rg -n -A65 -B10 'def _check_layout_case_collisions|def migrate_yaml_layout' slayer/storage/v4_migration.py

printf '%s\n' '=== Deterministic migration regression test ==='
rg -n -A75 -B10 'listdir|memories.*case|case.*memories|migrate_memories_layout' tests/test_id_collision.py tests/test_yaml_exact_entry.py

printf '%s\n' '=== Custom backend documentation contract ==='
rg -n -A100 -B10 'class MyCustomStorage|_save_model_impl|_delete_model_row|_save_datasource_impl|_delete_datasource_row' docs/configuration/storage.md

printf '%s\n' '=== Current collision flags and SQLite behavior ==='
rg -n -A20 -B8 '_ids_collide_as_filenames|check_datasource_id_collision|_check_model_identity_collision' slayer/storage/base.py slayer/storage/yaml_storage.py slayer/storage/sqlite_storage.py slayer/storage/join_sync.py

Length of output: 40436


@whimo Verified against the current PR:

  • migrate_memories_layout now detects pre-existing case-colliding .md stems before writing, and test_existing_on_disk_memory_pair_refused deterministically covers the behavior with a patched os.listdir.
  • The custom backend example consistently uses async def.
  • The cited multi-parameter helper calls use keyword arguments.
  • Collision rejection is now consistently YAML-only: YAMLStorage enables _ids_collide_as_filenames, while the base default leaves SQLite case-variant identities distinct. JoinSyncStorage correctly inherits the setting from its wrapped backend.

The SQLite atomicity and uniform-rejection findings are stale under that revised contract. The datasource write-lock and PermissionError handling points are not regressions introduced by this PR. No further changes requested.


✏️ Learnings added
Learnt from: whimo
URL: https://github.com/MotleyAI/slayer/pull/252

Timestamp: 2026-07-22T15:09:03.691Z
Learning: In Slayer storage, case-only identifier collision rejection is intentionally enabled only for filename-backed YAML storage through `StorageBackend._ids_collide_as_filenames` (set by `YAMLStorage` and propagated by `JoinSyncStorage`). SQLite intentionally permits case-variant datasource, model, and memory identities because its keys preserve them distinctly.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

@whimo whimo changed the title Reject case-colliding ids across all storage backends Reject case-colliding ids when using YAMLStorage Jul 22, 2026
@whimo
whimo merged commit 83e7039 into main Jul 22, 2026
5 of 6 checks passed
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.

YAMLStorage silently aliases datasources, models, and memories whose IDs are filesystem-equivalent

1 participant