agentscope-ai/agentscope · error · RuntimeError

Credential {credential_id!r} for owner {owner_id!r} disappea

Error message

Credential {credential_id!r} for owner {owner_id!r} disappeared immediately after a successful upsert.

What it means

Raised when a credential that was just successfully upserted cannot be read back from storage immediately afterwards. The code deliberately re-reads the record after upsert to refresh its value, and a None result means a concurrent delete (or a storage-layer inconsistency) removed it between the write and the read. It is a plain RuntimeError rather than an HTTPException because it indicates a server-side invariant violation, not a caller mistake.

Source

Thrown at src/agentscope/app/_router/_credential.py:154

            caller; 403 if visible but only readable.
    """
    owner_id, _ = await access.resolve_for_edit(
        user_id,
        ResourceKind.CREDENTIAL,
        credential_id,
    )

    credential = CredentialFactory.from_dict(body.data)
    credential.id = credential_id
    await storage.upsert_credential(owner_id, credential)
    # ``resolve_for_edit`` proved the record existed under ``owner_id``
    # and the upsert above just wrote back to the same key, so the read
    # is a value refresh, not an existence check. If it still comes back
    # empty (e.g. a concurrent delete), surface an explicit server error
    # rather than relying on ``assert`` (which ``-O`` strips).
    updated = await storage.get_credential(owner_id, credential_id)
    if updated is None:
        raise RuntimeError(
            f"Credential {credential_id!r} for owner {owner_id!r} "
            "disappeared immediately after a successful upsert.",
        )
    # Only reachable via ``resolve_for_edit``, so the caller has edit
    # permission by construction.
    return CredentialView.model_validate(
        {**updated.model_dump(), "editable": True},
    )


@credential_router.delete(
    "/{credential_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Delete a credential",
)
async def delete_credential(
    credential_id: str,
    user_id: str = Depends(get_current_user_id),

View on GitHub (pinned to e90f1c7592)

Solutions

  1. Check for concurrent delete requests against the same (owner_id, credential_id) and serialize them (e.g. retry the update after the delete completes, or use optimistic concurrency)
  2. Verify your storage backend actually persists the upsert before the subsequent get (check for rollback, failed flush, or key mismatch in a custom Storage implementation)
  3. If using a read replica, ensure read-your-writes consistency for this path
  4. Report as a server bug if reproducible with the built-in storage and no concurrent deletes
Defensive patterns

Strategy: retry

Try / catch

try:
    await client.update_credential(owner_id, credential_id, patch)
except RuntimeError as e:
    if "disappeared immediately" in str(e):
        await asyncio.sleep(0.2)
        await client.update_credential(owner_id, credential_id, patch)  # or re-read state
    else:
        raise

Prevention

When it happens

Trigger: Calling the credential update API while another request concurrently deletes the same credential_id for the same owner; or a broken/inconsistent storage backend where the upsert does not persist the key it claims to have written.

Common situations: Race conditions in tests that delete credentials in parallel; a mock or in-memory storage implementation whose upsert/get keys don't match; a database replication lag scenario where the read hits a stale replica.

Related errors


AI-assisted analysis of agentscope-ai/agentscope@e90f1c7592 (2026-08-28). Data as JSON: /api/errors/f60019ea051faa3b. Report an issue: GitHub.