Every product showed "Availability unknown". No errors, no warnings, no stack traces. The root cause was a single missing line in docker-compose.yml — and why it was silent made it take far too long to find.
ShopAgent's product cards show delivery estimates like "3–5 business days" and stock status like "In stock". For weeks during development, every single product showed "Availability unknown" instead. The inventory A2A agent was running. The orchestrator was calling it. No errors appeared in any log.
This is a debugging story about a silent failure in SQLite, a missing Docker volume mount, and the principle that the absence of an error is not the same as the presence of success.
WHAT THE USER SAW
Every product card showed:
🚚 Availability unknown
The product name, price, rating, and description were all correct. Stock status showed as "Out of stock" or "Availability unknown" on every item. "Add to Cart" buttons were missing. The shopping flow was unusable.
TRACING THE DATA FLOW
The delivery estimate on a product card comes from the estimated_delivery field on ProductCandidate. That field is populated in the search_products node:
estimated_delivery=inventory.get(p["id"], {}).get(
"estimated_delivery",
os.getenv("DEFAULT_DELIVERY_ESTIMATE", "Availability unknown"),
),
If inventory.get(p["id"]) returns an empty dict — meaning the inventory agent returned no data for this product — the fallback is the DEFAULT_DELIVERY_ESTIMATE environment variable, defaulting to "Availability unknown".
So the delivery estimate was falling back. That means the inventory agent's response was missing data for every product. Let us look at what the inventory agent returned.
The A2A response from the inventory agent contained entries, but each entry had estimated_delivery: "Currently unavailable — out of stock". Stock count was 0 for everything.
The shipping provider's estimate() method receives the stock count and uses it to determine the delivery string:
# In MockShippingProvider
async def estimate(self, product_id, stock, destination_region=""):
if stock == 0:
return ShippingEstimate(
estimated_delivery="Currently unavailable — out of stock",
carrier="mock",
)
return ShippingEstimate(
estimated_delivery="3–5 business days",
carrier="mock",
)
Stock was 0 for every product. The shipping provider was working correctly. The inventory agent was reporting stock=0 for everything.
THE SILENT SQLITE FAILURE
The inventory agent queries stock from a SQLite database:
DB_PATH = os.getenv("DB_PATH", "/data/catalog/products.db")
async with aiosqlite.connect(DB_PATH) as db:
async with db.execute(
f"SELECT id, stock FROM products WHERE id IN ({placeholders})",
product_ids,
) as cursor:
rows = await cursor.fetchall()
stock_map = {row[0]: int(row[1]) for row in rows}
The stock_map was empty. No rows were returned. That means the SELECT query found no products. That means the database was empty.
But the MCP catalogue server seeds the database on startup with 23 products. How was the database empty?
WHY NO ERROR WAS RAISED
aiosqlite.connect(path) does not raise an error if the file does not exist. SQLite creates a new empty database file at that path.
The inventory container had DB_PATH=/data/catalog/products.db. But /data/catalog/ did not exist in the inventory container's filesystem. When aiosqlite.connect was called with that path, it silently created a new, empty database at that location. The SELECT query ran successfully against an empty table. Zero rows returned. No exception.
The inventory agent processed each product ID and found stock=0 (the fallback when the product is not in stock_map). It returned valid JSON with all products listed, each with stock_count: 0. No error, no warning, no stack trace anywhere.
The root cause: the catalog-data Docker volume was mounted into the MCP catalogue container but not into the inventory container.
THE DB_DEGRADED FLAG
The inventory agent has a _meta.db_degraded marker in its response:
results = {
"_meta": {
"db_degraded": db_degraded,
"availability_unknown": db_degraded,
}
}
db_degraded is set to True when aiosqlite.connect() raises an exception. It is False when the connection succeeds — even if the database is empty.
This is the subtle part. An empty database is a valid SQLite database. The connection succeeds. db_degraded remains False. The orchestrator has no signal that something is wrong with the inventory data. It receives stock counts of 0 and trusts them.
The fix to the db_degraded flag for an empty database would be to check whether any rows were returned at all and flag it if product IDs were requested but none were found. We added this as a hardening measure after diagnosing the bug, but the real fix was the volume mount.
THE FIX
One line in docker-compose.yml:
shop-a2a-inventory:
build: ./backend
command: python -m agents.inventory
environment:
DB_PATH: /data/catalog/products.db
volumes:
- catalog-data:/data/catalog # This line was missing
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
After adding the volume mount and rebuilding, the inventory container mounted the same named volume as the MCP catalogue container. Both containers now read from /data/catalog/products.db, which is the same physical file on the Docker host. Stock counts were real. Delivery estimates appeared. "Add to Cart" buttons returned.
LESSONS FOR MULTI-CONTAINER DATA SHARING
Verify shared data is actually shared. Do not assume a volume mount is in place because the environment variable references the right path. Check the volumes: section explicitly for every container that needs it.
SQLite silently creates empty databases. If your containerised application uses SQLite with a path that depends on a volume mount, add a startup check that verifies the database contains the expected tables or a minimum number of rows. A seeded database with zero products at startup is a signal that the volume is missing.
Zero is a valid result. When an API returns stock=0, there are two possible meanings: the product is genuinely out of stock, or the data source was unavailable. These must be distinguished explicitly — either by a degraded flag, a separate error field, or a null value. A zero from an empty database looks identical to a zero from a full database that has sold out.
The absence of errors does not mean success. The inventory agent returned valid JSON. The orchestrator received a 200 OK response. No logs showed any problem. The failure was silent because every layer was working correctly on incorrect data.
THE TAKEAWAY
Volume mount bugs in Docker are among the hardest class of infrastructure bugs to diagnose because they are silent. The application runs. The APIs respond. The data is wrong. There is no stack trace pointing to the problem.
The pattern to follow: for every container that reads shared data, explicitly verify in code (not just in config) that the data is present and valid at startup. A cheap SELECT COUNT(*) FROM products at startup — failing fast if the result is 0 — would have surfaced this bug on the first run instead of weeks later.
The ShopAgent demo is live at https://shop-agent.agilecreativeminds.nl. See the demo showcase or follow the demo walkthrough. Built by Agile Creative Minds.