Say plainly when several live models hold different adapters

Round three of the adapter review found two leftovers from the previous fix.

With three or more live runtimes the reason read "another live model still
holds adapter-a and adapter-b", a singular subject attributing several
adapters to one model, when each runtime attaches one adapter per kind. The
subject now agrees in number with what is held.

LoadRecord still stored its own copy of the manifest sha256, which is the
same second source of truth this work removed for kind, adapter_id and path:
the only constructor copied it, and nothing stopped a caller from passing a
hash that disagreed with the source handed in. It is a property now, so
runtime.py keeps reading record.sha256 and there is one value.

Also recorded, measured by the reviewer and deliberately not treated as
defects: a row for a kind with nothing on disk now reports base_model as null
rather than the runtime's model id, since that row describes no adapter at
all; the same adapter id can appear in both a loaded row and a shadowed row
when it is attached while another copy wins the mount, and both rows are
true; and when the attached adapter is deleted from disk the row says missing
with null identity instead of claiming a gone model.

Three tests carry this: the three-runtime case asserts the plural wording, and
both fixes were mutation-checked (dropping the plural branch fails two tests,
restoring the stored hash fails the generation test).

pytest -q: 228 passed. ruff clean. mypy cold cache on the three touched source
files: 2 errors, identical to the stashed HEAD tree, net zero. pyright: 2
errors, both the known qwen_vl_utils tuple artifact.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 00:51:01 -04:00
1 parent 7ec25bf629
commit c48b48b80f
3 files changed
+80 -20

No files matched your search

+18 -6
View File
@@ -37,7 +37,6 @@ class LoadRecord:
"""One adapter this process attached to one model."""
source: AdapterSource
sha256: str
@property
def kind(self) -> TrainingAdapterKind:
@@ -51,6 +50,10 @@ class LoadRecord:
def path(self) -> str:
return str(self.source.path)
@property
def sha256(self) -> str:
return self.source.manifest.sha256
_ATTACHMENTS: list[tuple[LoadRecord, weakref.ReferenceType[object]]] = []
@@ -63,7 +66,9 @@ def read_manifest(path: Path) -> AdapterManifest:
try:
return AdapterManifest.model_validate_json(text)
except ValidationError as error:
raise ValueError(f"{path}: the adapter manifest is not a valid AdapterManifest ({error})") from error
raise ValueError(
f"{path}: the adapter manifest is not a valid AdapterManifest ({error})"
) from error
def cartridge_roots(database: Database) -> dict[str, Path]:
@@ -111,7 +116,9 @@ def discover(settings: Settings, database: Database) -> tuple[list[AdapterSource
for cartridge_id in mounted:
root = roots.get(cartridge_id)
if root is None:
logger.warning("cartridge %s is mounted but its directory is not registered", cartridge_id)
logger.warning(
"cartridge %s is mounted but its directory is not registered", cartridge_id
)
continue
for manifest_path in sorted((root / "adapters").glob(f"*/{MANIFEST_FILE}")):
sources.append(
@@ -144,7 +151,11 @@ def resolve(
)
ready = [source for source in matching if (source.path / WEIGHTS_FILE).exists()]
if not ready:
return None, "deferred", f"the {kind.value} manifests are present but none carries {WEIGHTS_FILE}"
return (
None,
"deferred",
f"the {kind.value} manifests are present but none carries {WEIGHTS_FILE}",
)
ready.sort(key=_preference, reverse=True)
chosen = ready[0]
if chosen.origin == "cartridge":
@@ -172,7 +183,7 @@ def live_attachments() -> list[LoadRecord]:
def record_load(source: AdapterSource, model: Any) -> LoadRecord:
record = LoadRecord(source=source, sha256=source.manifest.sha256)
record = LoadRecord(source=source)
_ATTACHMENTS[:] = [
(item, reference) for item, reference in _ATTACHMENTS if reference() is not None
]
@@ -208,7 +219,8 @@ def plan(settings: Settings, database: Database) -> AdapterLoadReport:
reason = f"attached to the live model from {newest.path}"
held = sorted({record.adapter_id for record in live[:-1] if record.adapter_id != newest.adapter_id})
if held:
reason = f"{reason}; another live model still holds {' and '.join(held)}"
subject = "another live model still holds" if len(held) == 1 else "other live models still hold"
reason = f"{reason}; {subject} {' and '.join(held)}"
rows.append(_choice(kind, state, reason, newest.source))
continue
if earlier is not None and source is not None: