fix: resolve startup NameError in vector-sync metrics task

The Starlette lifespan started `vector_sync_metrics_task` with undefined
names `task_producer` and `receive_stream`. Those locals only exist inside
the `_wire_vector_sync_state` helper; in the lifespan the transport is bound
as `ingest_transport`. The undefined reference raised `NameError`, which
aborted the background-sync task group and crashed startup in every
deployment mode ("Application startup failed. Exiting.").

Introduced by fbe70ecd ("feat: backend-agnostic vector-sync gauges").

Pass `ingest_transport.producer` / `ingest_transport.receive_stream` at both
call sites (single-user app.py:1791, OAuth/login-flow app.py:2012).

Also fix 10 pre-existing `ty` possibly-missing-attribute diagnostics: the
deck indexing code in scanner.py, processor.py and search/context.py reads
full-DeckCard-only fields (description, type, owner, etag, lastModified) off
`stack.cards`, typed `list[DeckCard | DeckCardSummary]`. Freshly-fetched
stacks from `get_stacks()` always hold full DeckCards (the summary
projection only happens in the tool layer), so narrow with
`cast(list[DeckCard], ...)` — matching the existing pattern in
server/deck.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-04 22:10:40 +02:00
co-authored by Claude Opus 4.8
parent c47467f769
commit 1f8b3ba95e
4 changed files with 20 additions and 8 deletions
+4 -4
View File
@@ -1790,8 +1790,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
# falls back to the procrastinate job counts there. # falls back to the procrastinate job counts there.
await tg.start( await tg.start(
vector_sync_metrics_task, vector_sync_metrics_task,
task_producer, ingest_transport.producer,
receive_stream, ingest_transport.receive_stream,
shutdown_event, shutdown_event,
) )
@@ -2011,8 +2011,8 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
# falls back to the procrastinate job counts there. # falls back to the procrastinate job counts there.
await tg.start( await tg.start(
vector_sync_metrics_task, vector_sync_metrics_task,
task_producer, ingest_transport.producer,
receive_stream, ingest_transport.receive_stream,
shutdown_event, shutdown_event,
) )
+6 -1
View File
@@ -6,12 +6,14 @@ position markers for better visualization and understanding of search results.
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import cast
from httpx import HTTPStatusError from httpx import HTTPStatusError
from qdrant_client.models import FieldCondition, Filter, MatchValue from qdrant_client.models import FieldCondition, Filter, MatchValue
from nextcloud_mcp_server.client import NextcloudClient from nextcloud_mcp_server.client import NextcloudClient
from nextcloud_mcp_server.config import get_settings from nextcloud_mcp_server.config import get_settings
from nextcloud_mcp_server.models.deck import DeckCard
from nextcloud_mcp_server.search.access_filter import build_ownership_filter from nextcloud_mcp_server.search.access_filter import build_ownership_filter
from nextcloud_mcp_server.utils.validation import is_valid_nextcloud_doc_id from nextcloud_mcp_server.utils.validation import is_valid_nextcloud_doc_id
from nextcloud_mcp_server.vector.html_processor import html_to_markdown from nextcloud_mcp_server.vector.html_processor import html_to_markdown
@@ -792,7 +794,10 @@ async def _fetch_document_text(
if card_found: if card_found:
break break
if stack.cards: if stack.cards:
for c in stack.cards: # get_stacks() always yields full DeckCard objects;
# the DeckCardSummary projection only happens in the
# tool layer, never on freshly-fetched stacks.
for c in cast(list[DeckCard], stack.cards):
if c.id == int(doc_id): if c.id == int(doc_id):
card = c card = c
card_found = True card_found = True
+5 -1
View File
@@ -18,6 +18,7 @@ from nextcloud_mcp_server.client import NextcloudClient
from nextcloud_mcp_server.config import get_settings from nextcloud_mcp_server.config import get_settings
from nextcloud_mcp_server.document_processors import get_registry from nextcloud_mcp_server.document_processors import get_registry
from nextcloud_mcp_server.embedding import get_bm25_service, get_embedding_service from nextcloud_mcp_server.embedding import get_bm25_service, get_embedding_service
from nextcloud_mcp_server.models.deck import DeckCard
from nextcloud_mcp_server.observability.metrics import ( from nextcloud_mcp_server.observability.metrics import (
record_document_chunks, record_document_chunks,
record_embedding, record_embedding,
@@ -421,7 +422,10 @@ async def _index_document(
if card_found: if card_found:
break break
if s.cards: if s.cards:
for c in s.cards: # get_stacks() always yields full DeckCard objects;
# the DeckCardSummary projection only happens in the
# tool layer, never on freshly-fetched stacks.
for c in cast(list[DeckCard], s.cards):
if c.id == int(doc_task.doc_id): if c.id == int(doc_task.doc_id):
card = c card = c
board = b board = b
+5 -2
View File
@@ -19,6 +19,7 @@ from qdrant_client.models import FieldCondition, Filter, MatchValue, Record
from nextcloud_mcp_server.client import NextcloudClient from nextcloud_mcp_server.client import NextcloudClient
from nextcloud_mcp_server.client.news import NewsItemType from nextcloud_mcp_server.client.news import NewsItemType
from nextcloud_mcp_server.config import get_settings from nextcloud_mcp_server.config import get_settings
from nextcloud_mcp_server.models.deck import DeckCard
from nextcloud_mcp_server.observability.metrics import record_vector_sync_scan from nextcloud_mcp_server.observability.metrics import record_vector_sync_scan
from nextcloud_mcp_server.observability.tracing import trace_operation from nextcloud_mcp_server.observability.tracing import trace_operation
from nextcloud_mcp_server.server.tag_exclusion import ( from nextcloud_mcp_server.server.tag_exclusion import (
@@ -1097,8 +1098,10 @@ async def scan_deck_cards(
if not stack.cards: if not stack.cards:
continue continue
# Iterate through cards in stack # Iterate through cards in stack. get_stacks() always yields full
for card in stack.cards: # DeckCard objects; the DeckCardSummary projection only happens in
# the tool layer (server/deck.py), never on freshly-fetched stacks.
for card in cast(list[DeckCard], stack.cards):
# Skip archived cards # Skip archived cards
if card.archived: if card.archived:
continue continue