Skip to content

feat(llm): SSOT extract + connector synthesis + conversation topic for desktop invent paths - #11286

Merged
undivisible merged 43 commits into
mainfrom
backend-ssot-kg-chat
Aug 10, 2026
Merged

feat(llm): SSOT extract + connector synthesis + conversation topic for desktop invent paths#11286
undivisible merged 43 commits into
mainfrom
backend-ssot-kg-chat

Conversation

@undivisible

@undivisible undivisible commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add return-only POST /v1/knowledge-graph/extract, POST /v1/memories/extract, POST /v1/connectors/synthesize and POST /v1/conversations/topic through managed Luna features (knowledge_graph / memories / conv_structure).
  • Wire Mac/Win save_knowledge_graph discovery_text, Win local KG synthesis, and Mac/Win memory-log import onto those endpoints instead of inventing graphs/memories via Haiku chat completions.
  • Wire Mac/Win calendar, gmail and notes/sticky-notes imports onto /v1/connectors/synthesize instead of building local Haiku synthesis prompts.
  • Route Windows fast conversation titling through /v1/conversations/topic; drop the now-dead ModelQoS.Claude.synthesis tier.
  • Add POST /v1/users/ai-profile/synthesize (both prompt stages + consolidation backend-side) and move Mac (direct Gemini) and Windows (Haiku) AI-profile generation onto it.
  • Key the gateway fake-provider smoke and replay oracle off the lane's declared providers instead of a hardcoded openai key.
  • Accept omi-structured on desktop chat and route it to omi:auto:chat-structured, moving the Windows automation planner and local-agent loop off their explicit Haiku model id.
  • Delete the dead direct-OpenAI mobile client (app/lib/backend/http/openai.dart) and its now-unused OPENAI_API_KEY env plumbing.

Review fixes on this head

  • save_knowledge_graph: extract only for a non-empty trimmed discovery_text, and fall back to supplied nodes/edges when extraction fails, so the compatibility path stays usable.
  • The five managed extraction routes are async, dispatch through run_blocking(llm_executor, ...), and refuse a paywalled account (402) before the provider call; KG extract uses strict_parse=True; the memory-log request cap matches the helper's 40k budget.
  • Extraction prompts put the contract in a system message and the untrusted payload (imported logs, Gmail metadata, notes) in a separate human message.
  • Structured-lane traffic is accounted to chat_structured + its own route instead of chat_agent.
  • macOS managed synthesis calls get a 60s per-request timeout, and profile synthesis pins an owner authorization snapshot.
  • Windows save_knowledge_graph forwards ctx.signal and moves to the long relay timeout class; long Sticky Note lines are chunked instead of truncated; the graph-extract fallback emits a structured degraded record.
  • Accept explicit Luna/auto managed chat lane ids in desktop triage.

Product invariants affected

  • INV-AGENT-*
  • INV-AUTH-1
  • INV-CHAT-1
  • INV-INT-1
  • INV-MEM-1

Failure class (fixes)

Failure-Class: none

Test plan

  • backend/.venv/bin/python -m pytest tests/unit/test_knowledge_graph_extract_route.py tests/unit/test_memories_extract_route.py tests/unit/test_memories_extract_helper.py tests/unit/test_rate_limiting.py::TestRouterPolicyMapping
  • Desktop onboarding memory-log paste import hits /v1/memories/extract
  • Desktop save_knowledge_graph with discovery_text hits /v1/knowledge-graph/extract
  • Win Settings rebuild local graph uses backend extract (no Haiku /v2/chat/completions)
  • backend/.venv/bin/python -m pytest tests/unit/test_connector_synthesis_helper.py tests/unit/test_connector_synthesis_route.py tests/unit/test_rate_limiting.py tests/unit/test_route_policy_inventory.py tests/unit/test_desktop_rest_inventory.py tests/unit/test_app_client_schema_inventory.py tests/unit/test_inventory_whitelist_honesty.py — 124 passed
  • desktop/windows vitest: calendarExtract, gmailExtract, stickyNotesImport — 12 passed; tsc --noEmit -p tsconfig.web.json clean
  • xcrun swift build -c debug --package-path Desktop — Build complete; xctest -XCTest ConnectorSynthesisResponseTests — 2 passed
  • backend/.venv/bin/python -m pytest tests/unit/test_conversation_topic_helper.py tests/unit/test_conversation_topic_route.py tests/unit/test_rate_limiting.py — 74 passed
  • vitest run conversationTopic.test.ts — 2 passed; xctest -XCTest ModelQoSTests — 16 passed
  • Desktop calendar/gmail/notes connector import hits /v1/connectors/synthesize
  • backend/.venv/bin/python -m pytest tests/unit/test_ai_user_profile_helper.py tests/unit/test_ai_user_profile_route.py — 9 passed
  • vitest run src/main/assistants/aiUserProfile — 28 passed; xctest -XCTest AIUserProfileSynthesisResponseTests — 2 passed
  • Windows finalized conversation shows a provisional title from /v1/conversations/topic
  • backend/.venv/bin/python -m pytest tests/unit/test_desktop_chat.py — 58 passed; vitest run src/renderer/src/lib src/main/assistants — 1954 passed
  • Desktop daily AI-profile generation hits /v1/users/ai-profile/synthesize
  • Review-fix pass: pytest on the ten helper/route suites + test_rate_limiting.py + test_memories_create.py — 129 passed; test_desktop_chat.py — 59 passed; vitest run src/main/agentKernel src/renderer/src/lib src/main/assistants — 2505 passed; swift build --build-tests + 20 Swift tests — 0 failures
  • Windows automation planner runs on the structured lane end to end

Note

Medium Risk
Large cross-surface change (new LLM routes, paywall gates, desktop chat lane accounting, and client import paths); mitigated by return-only contracts, rate limits, and extensive unit tests, but end-to-end desktop flows still need manual verification.

Overview
Moves desktop “invent via Haiku/Gemini” LLM work onto return-only backend routes backed by managed Luna features, with clients persisting through existing write APIs.

New Firebase-authenticated endpoints (rate limits, trial paywall 402, llm_executor, 502 on failure): POST /v1/knowledge-graph/extract, /v1/memories/extract, /v1/connectors/synthesize, /v1/conversations/topic, and /v1/users/ai-profile/synthesize. Prompts live in new utils/llm/* helpers; untrusted user text is sent in a separate human turn from the system contract.

Desktop/macOS stops local synthesis prompts for calendar/gmail/notes imports, memory-log onboarding, AI user profile (was Gemini + local two-stage prompts), and routes save_knowledge_graph through backend extract when discovery_text is set (falls back to supplied nodes/edges if extract fails). ModelQoS.Claude.synthesis is removed; managed synthesis calls can use a longer POST timeout.

Desktop chat gateway accepts omi-structured / omi:auto:chat-structured and attributes that lane as chat_structured (not chat_agent). Session-title output budget is keyed by feature, not provider.

Mobile app drops dead direct OpenAI client code and OPENAI_API_KEY env wiring.

Policy inventory, OpenAPI export, LLM surface inventory, and gateway smoke/replay fakes are updated; broad unit test coverage added for helpers and routes.

Reviewed by Cursor Bugbot for commit 1aaaaff. Configure here.

undivisible and others added 12 commits August 9, 2026 10:59
Add return-only /v1/knowledge-graph/extract through get_llm(knowledge_graph),
wire desktop save_knowledge_graph to prefer discovery_text via that endpoint,
and accept explicit Luna/auto lane ids for managed desktop chat.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add return-only POST /v1/memories/extract through get_llm('memories') and route
Mac/Win memory-log import plus Win local KG synthesis through backend extract
instead of Anthropic Haiku chat completions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Failure-Class: none
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Failure-Class: none
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
discovery_text is preferred and nodes/edges are no longer required.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_dae915fc-895b-4728-a54e-70abeefe149f)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

9 issues found across 42 files

Confidence score: 3/5

  • In backend/utils/llm/knowledge_graph.py and desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift, repeated discovery_text saves can assign fresh IDs to entities that already exist, fragmenting the local graph and weakening incremental merge quality across sessions — derive deterministic IDs (uid+normalized label) and resolve extracted labels/aliases to existing local node IDs before merge.
  • backend/utils/llm/knowledge_graph.py (extraction_to_client_graph) and desktop/windows/src/main/agentKernel/productToolExecutors.ts can mis-handle extracted entities: shared label/alias ID mapping can collapse distinct nodes while appending duplicates, and Windows type filtering can drop backend-emitted node types, leading to missing or incorrect KG structure — split node/edge ID bookkeeping and broaden or map unsupported node types before save.
  • In backend/routers/desktop_chat.py, Luna/auto lane IDs can end up with incorrect gateway_mode when gateway is disabled or BYOK Anthropic is set, which risks requests being routed through the wrong path and failing unexpectedly — align gateway_mode derivation with lane/config combinations in all branches.
  • Memory extraction limits are inconsistent between backend/routers/memories.py and backend/utils/llm/memories.py: very large existing_memories can still blow context/work budgets, and input is silently truncated to 40k despite a 100k contract, causing hidden recall loss or provider failures — enforce per-entry/aggregate prompt budgets and make endpoint limits/truncation behavior explicit and consistent.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/routers/desktop_chat.py">

<violation number="1" location="backend/routers/desktop_chat.py:233">
P2: The new Luna/auto lane ids ('omi-luna', 'omi-auto', 'omi:auto:chat-agent') route to the managed gateway when it is active, but when the gateway feature is off or a BYOK Anthropic key is configured, `gateway_mode` is forced false and `_request()` rejects these models with HTTP 400 'unsupported model' (they are not in `_MODEL_ROUTES`). Desktop traffic that starts sending these aliases (per the PR intent of 'accept explicit Luna/auto managed chat lane ids') will therefore hard-fail in any non-gateway/BYOK environment, where the previous Sonnet aliases (e.g. 'omi-sonnet', which are in `_MODEL_ROUTES`) still worked on the direct path. Consider adding the aliases to `_MODEL_ROUTES` (mapping to the Sonnet model) so the direct/BYOK fallback remains functional, or explicitly fail over to the direct Sonnet path when gateway mode is unavailable, rather than surfacing an 'unsupported model' 400.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift:2846">
P2: Repeated `discovery_text` saves create new IDs for already-present entities, fragmenting the incremental local graph; resolve extracted labels/aliases to existing local node IDs before merging.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift:2846">
P3: Discovery-text saves report zero nodes and edges in `onboardingChatToolUsed`; derive analytics counts from resolved extraction/save result so onboarding graph telemetry reflects saved data.</violation>
</file>

<file name="backend/routers/memories.py">

<violation number="1" location="backend/routers/memories.py:472">
P2: Oversized `existing_memories` entries can create provider-context failures or excessive request work despite the 200-item cap; bound each entry and the aggregate prompt budget before calling the extractor.</violation>
</file>

<file name="desktop/macos/e2e/flows/chat-first-cohesive.yaml">

<violation number="1" location="desktop/macos/e2e/flows/chat-first-cohesive.yaml:27">
P3: The added `covers:` entry claims this flow exercises "save_knowledge_graph discovery_text resolves through backend KG extract SSOT", but none of the chat-first-cohesive steps (`chat_first_render_fixture_task_card` renders a task card, S14-S16 only start/discuss a capture turn) invoke `save_knowledge_graph` with `discovery_text`, so `KnowledgeGraphToolSupport.resolveDiscoveryText` is never reached. List a file under a flow's `covers:` only when the flow actually exercises that path; this file is already covered by `memory-graph.yaml`, so the entry is both inaccurate and redundant.</violation>
</file>

<file name="backend/utils/llm/knowledge_graph.py">

<violation number="1" location="backend/utils/llm/knowledge_graph.py:174">
P2: Repeated extraction creates a new local node for the same label because client graph IDs are random on every request; derive IDs deterministically from `uid` and normalized label so `save_knowledge_graph` can merge repeated discoveries.</violation>

<violation number="2" location="backend/utils/llm/knowledge_graph.py:174">
P2: In `extraction_to_client_graph`, node ids and edge ids are derived from a single shared `label_to_node_id` map keyed on lowercased label plus every alias, but each node/edge is still appended to the returned arrays. Distinct entities that share a label (e.g. the LLM returns two 'neo' nodes, or one node labeled 'City' collides with another node '$Zion' having alias 'City') collapse to the same `node_id` while both are emitted, so the returned graph contains duplicate nodes carrying the same id; likewise two edges between the same source/target with the same label share one `edge_id`. Because this graph is the input to desktop `save_knowledge_graph` persistence, colliding ids can cause one entity to overwrite another on upsert or drop a relationship. Consider deduplicating nodes by label/alias (emit at most one node per resolved id) and making edge ids unique per instance, e.g. by including an occurrence index.</violation>
</file>

<file name="backend/utils/llm/memories.py">

<violation number="1" location="backend/utils/llm/memories.py:286">
P2: The endpoint advertises that it accepts up to 100,000 characters of text, but the extraction helper silently truncates the input to the first 40,000 characters via `content[:40_000]`. A desktop onboarding/import that pastes a log between 40k and 100k will get a successful 200 with memories extracted only from the head of the log — tail facts are dropped with no signal to the caller. Consider aligning the request contract with the processing cap (e.g. also `max_length=40_000` on the route) or returning a truncation flag in the response so clients know part of the log was not considered.</violation>
</file>

<file name="desktop/windows/src/main/agentKernel/productToolExecutors.ts">

<violation number="1" location="desktop/windows/src/main/agentKernel/productToolExecutors.ts:1088">
P2: The new `discovery_text` path saves backend-extracted nodes through `KG_NODE_TYPES`, which only accepts person/organization/place/thing/concept. But this same PR's `kgSynthesis.mapBackendNodeType` proves the backend extract can return `node_type` of `project`, `interest`, and `org` — none of which are in that set. When a real agent calls `save_knowledge_graph` with `discovery_text`, any project/interest/org entity the backend returns is silently coerced to `thing` in the saved graph, so those entities lose their semantic type. Worth aligning the two consumers on one node_type vocabulary (e.g. extend `KG_NODE_TYPES` to cover project/interest/org) before merge.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/routers/desktop_chat.py Outdated
normalized = model.strip().lower()
if not normalized:
return True
if normalized in _MANAGED_CHAT_ALIASES:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The new Luna/auto lane ids ('omi-luna', 'omi-auto', 'omi:auto:chat-agent') route to the managed gateway when it is active, but when the gateway feature is off or a BYOK Anthropic key is configured, gateway_mode is forced false and _request() rejects these models with HTTP 400 'unsupported model' (they are not in _MODEL_ROUTES). Desktop traffic that starts sending these aliases (per the PR intent of 'accept explicit Luna/auto managed chat lane ids') will therefore hard-fail in any non-gateway/BYOK environment, where the previous Sonnet aliases (e.g. 'omi-sonnet', which are in _MODEL_ROUTES) still worked on the direct path. Consider adding the aliases to _MODEL_ROUTES (mapping to the Sonnet model) so the direct/BYOK fallback remains functional, or explicitly fail over to the direct Sonnet path when gateway mode is unavailable, rather than surfacing an 'unsupported model' 400.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/desktop_chat.py, line 233:

<comment>The new Luna/auto lane ids ('omi-luna', 'omi-auto', 'omi:auto:chat-agent') route to the managed gateway when it is active, but when the gateway feature is off or a BYOK Anthropic key is configured, `gateway_mode` is forced false and `_request()` rejects these models with HTTP 400 'unsupported model' (they are not in `_MODEL_ROUTES`). Desktop traffic that starts sending these aliases (per the PR intent of 'accept explicit Luna/auto managed chat lane ids') will therefore hard-fail in any non-gateway/BYOK environment, where the previous Sonnet aliases (e.g. 'omi-sonnet', which are in `_MODEL_ROUTES`) still worked on the direct path. Consider adding the aliases to `_MODEL_ROUTES` (mapping to the Sonnet model) so the direct/BYOK fallback remains functional, or explicitly fail over to the direct Sonnet path when gateway mode is unavailable, rather than surfacing an 'unsupported model' 400.</comment>

<file context>
@@ -205,26 +205,33 @@ async def bounded_receive():
+    normalized = model.strip().lower()
+    if not normalized:
+        return True
+    if normalized in _MANAGED_CHAT_ALIASES:
+        return True
     if normalized in _MODEL_ROUTES:
</file context>

discoveryText, expectedOwnerId: expectedOwnerID)
{
case .success(let graph):
nodesArray = graph.nodes

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Repeated discovery_text saves create new IDs for already-present entities, fragmenting the incremental local graph; resolve extracted labels/aliases to existing local node IDs before merging.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift, line 2846:

<comment>Repeated `discovery_text` saves create new IDs for already-present entities, fragmenting the incremental local graph; resolve extracted labels/aliases to existing local node IDs before merging.</comment>

<file context>
@@ -2836,10 +2836,22 @@ class ChatToolExecutor {
+        discoveryText, expectedOwnerId: expectedOwnerID)
+      {
+      case .success(let graph):
+        nodesArray = graph.nodes
+        edgesArray = graph.edges
+      case .failure(let message):
</file context>

Comment thread backend/routers/memories.py Outdated

text: str = Field(..., min_length=1, max_length=100_000)
text_source: str = Field(default="memory_log", min_length=1, max_length=64)
existing_memories: List[str] = Field(default_factory=list, max_length=200)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Oversized existing_memories entries can create provider-context failures or excessive request work despite the 200-item cap; bound each entry and the aggregate prompt budget before calling the extractor.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/memories.py, line 472:

<comment>Oversized `existing_memories` entries can create provider-context failures or excessive request work despite the 200-item cap; bound each entry and the aggregate prompt budget before calling the extractor.</comment>

<file context>
@@ -464,6 +464,48 @@ def _validate_mutable_memory(uid: str, memory_id: str, *, db_client: Any) -> Mem
+
+    text: str = Field(..., min_length=1, max_length=100_000)
+    text_source: str = Field(default="memory_log", min_length=1, max_length=64)
+    existing_memories: List[str] = Field(default_factory=list, max_length=200)
+
+
</file context>

label_to_node_id: Dict[str, str] = {}
nodes: List[Dict[str, Any]] = []
for node in extraction.nodes:
node_id = label_to_node_id.get(node.label.lower()) or str(uuid.uuid4())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Repeated extraction creates a new local node for the same label because client graph IDs are random on every request; derive IDs deterministically from uid and normalized label so save_knowledge_graph can merge repeated discoveries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/utils/llm/knowledge_graph.py, line 174:

<comment>Repeated extraction creates a new local node for the same label because client graph IDs are random on every request; derive IDs deterministically from `uid` and normalized label so `save_knowledge_graph` can merge repeated discoveries.</comment>

<file context>
@@ -135,62 +155,133 @@ def extract_knowledge_from_memory(
+    label_to_node_id: Dict[str, str] = {}
+    nodes: List[Dict[str, Any]] = []
+    for node in extraction.nodes:
+        node_id = label_to_node_id.get(node.label.lower()) or str(uuid.uuid4())
+        label_to_node_id[node.label.lower()] = node_id
+        for alias in node.aliases:
</file context>

Comment thread desktop/macos/e2e/flows/memory-graph.yaml Outdated
Comment thread desktop/macos/Desktop/Sources/Generated/GeneratedToolCapabilities.swift Outdated
Comment thread desktop/windows/src/main/agentKernel/productToolExecutorsTierB.test.ts Outdated
discoveryText, expectedOwnerId: expectedOwnerID)
{
case .success(let graph):
nodesArray = graph.nodes

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Discovery-text saves report zero nodes and edges in onboardingChatToolUsed; derive analytics counts from resolved extraction/save result so onboarding graph telemetry reflects saved data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift, line 2846:

<comment>Discovery-text saves report zero nodes and edges in `onboardingChatToolUsed`; derive analytics counts from resolved extraction/save result so onboarding graph telemetry reflects saved data.</comment>

<file context>
@@ -2836,10 +2836,22 @@ class ChatToolExecutor {
+        discoveryText, expectedOwnerId: expectedOwnerID)
+      {
+      case .success(let graph):
+        nodesArray = graph.nodes
+        edgesArray = graph.edges
+      case .failure(let message):
</file context>

- desktop/macos/Desktop/Sources/Chat/AgentClient.swift
- desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift
# save_knowledge_graph discovery_text resolves through backend KG extract SSOT.
- desktop/macos/Desktop/Sources/Services/KnowledgeGraphToolSupport.swift

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The added covers: entry claims this flow exercises "save_knowledge_graph discovery_text resolves through backend KG extract SSOT", but none of the chat-first-cohesive steps (chat_first_render_fixture_task_card renders a task card, S14-S16 only start/discuss a capture turn) invoke save_knowledge_graph with discovery_text, so KnowledgeGraphToolSupport.resolveDiscoveryText is never reached. List a file under a flow's covers: only when the flow actually exercises that path; this file is already covered by memory-graph.yaml, so the entry is both inaccurate and redundant.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/e2e/flows/chat-first-cohesive.yaml, line 27:

<comment>The added `covers:` entry claims this flow exercises "save_knowledge_graph discovery_text resolves through backend KG extract SSOT", but none of the chat-first-cohesive steps (`chat_first_render_fixture_task_card` renders a task card, S14-S16 only start/discuss a capture turn) invoke `save_knowledge_graph` with `discovery_text`, so `KnowledgeGraphToolSupport.resolveDiscoveryText` is never reached. List a file under a flow's `covers:` only when the flow actually exercises that path; this file is already covered by `memory-graph.yaml`, so the entry is both inaccurate and redundant.</comment>

<file context>
@@ -23,6 +23,8 @@ covers:
   - desktop/macos/Desktop/Sources/Chat/AgentClient.swift
   - desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift
+  # save_knowledge_graph discovery_text resolves through backend KG extract SSOT.
+  - desktop/macos/Desktop/Sources/Services/KnowledgeGraphToolSupport.swift
   - desktop/macos/Desktop/Sources/Providers/ChatFirstBlockToolExecutor.swift
   - desktop/macos/Desktop/Sources/Chat/ChatFirstBlockValidation.swift
</file context>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49fb99165a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
}
return try await post(
"v1/memories/extract",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate desktop release on the new extract endpoints

When a macOS candidate is released before the independently deployed Python backend contains this route, every real memory-log import now receives a 404 and returns .failed; the same rollout dependency exists for the new knowledge-graph call. I checked gcp_backend_auto_dev.yml, desktop_auto_release.yml, and desktop_qualify_beta.yml: backend deployment is downstream of Release Eligibility, while release planning waits only for that eligibility check and qualification probes /v1/health, not either new route, so a failed or still-running deployment does not block promotion. Add an exact endpoint-capability gate before releasing the client.

AGENTS.md reference: desktop/macos/AGENTS.md:L71-L71

Useful? React with 👍 / 👎.

Comment on lines +32 to +34
if (t === 'project') return 'project'
if (t === 'interest') return 'interest'
if (t === 'place' || t === 'thing' || t === 'concept') return 'interest'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve project nodes in the SSOT mapping

For Windows graph rebuilds containing project evidence, the new backend contract advertises only person, place, thing, concept, and organization (backend/utils/llm/knowledge_graph.py), whereas the previous buildSynthesisPrompt explicitly requested project nodes. Mapping every returned thing or concept to interest therefore turns projects into interests, so project-specific graph nodes disappear from normal SSOT output. Extend the shared extraction type contract to represent projects instead of relying on a value the backend prompt does not request.

Useful? React with 👍 / 👎.

Comment on lines +1076 to +1080
const extracted = await backendJsonFetch({
method: 'POST',
path: '/v1/knowledge-graph/extract',
body: { text: discoveryText, include_existing: false },
timeoutMs: 60_000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Propagate relay cancellation to graph extraction

When the relay socket disconnects during this new network-backed tool call, the executor ignores its ProductToolContext and does not pass ctx.signal to backendJsonFetch. The request can therefore continue for the full 60-second timeout and then execute upsertLocalGraph even though the originating run can no longer receive the result; this violates the existing executor contract that disconnects abort in-flight work. Accept ctx, pass its signal into the request, and check cancellation before the local write.

Useful? React with 👍 / 👎.

Comment thread backend/routers/knowledge_graph.py Outdated
Comment on lines +205 to +209
extraction = getattr(kg_mod, "extract_kg_from_text")(
uid,
body.text,
user_name=user_name,
load_existing_from_db=body.include_existing,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fail the route when knowledge-graph parsing fails

When Luna returns malformed structured output, extract_kg_from_text uses its default strict_parse=False and converts the parse failure into an empty extraction, so this new HTTP route returns 200 with empty arrays instead of the intended 502. Windows then treats the request as successful, saves only the deterministic floor, and stamps the graph fresh for 12 hours, suppressing a retry. Pass strict_parse=True at this HTTP boundary so provider/parse failures remain distinguishable from a valid no-entities result.

Useful? React with 👍 / 👎.

label_to_node_id: Dict[str, str] = {}
nodes: List[Dict[str, Any]] = []
for node in extraction.nodes:
node_id = label_to_node_id.get(node.label.lower()) or str(uuid.uuid4())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Derive deterministic IDs for extracted graph entities

Whenever the same entity is extracted in more than one discovery_text call, this generates a fresh UUID even for an identical normalized label. Both macOS local_kg_nodes and Windows onboarding_kg_nodes upsert only by ID, so repeated onboarding discoveries create duplicate nodes and new duplicate edges rather than merging the entity as the tool contract promises. Derive IDs deterministically from the canonical label/type, or reconcile against the caller's existing graph before returning them.

Useful? React with 👍 / 👎.

Comment thread backend/routers/knowledge_graph.py Outdated
tags=['knowledge_graph'],
response_model=ExtractKnowledgeGraphResponse,
)
def extract_knowledge_graph(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Dispatch extraction calls through the LLM executor

Under concurrent extract requests from multiple users, these synchronous FastAPI handlers hold Starlette's shared sync-route worker pool while get_llm(...).invoke() waits on the provider; the memories endpoint is implemented the same way. A slow provider can therefore exhaust the general pool and starve unrelated synchronous API routes despite the repository's dedicated six-worker LLM lane. Make the handlers async and offload each extraction with await run_blocking(llm_executor, ...).

AGENTS.md reference: backend/AGENTS.md:L283-L292

Useful? React with 👍 / 👎.

Comment on lines +133 to +134
} catch (e) {
console.warn('[kg] synthesis LLM call failed; saving deterministic floor only', e)
console.warn('[kg] backend knowledge-graph extract failed; saving deterministic floor only', e)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Record the degraded knowledge-graph fallback

When the new backend extract call fails, Windows silently switches from semantic extraction to a deterministic-only graph and still persists that degraded result, but the branch emits only a renderer console warning. This provider/correctness fallback needs the shared bounded recordFallback telemetry so backend-route outages and authentication failures are operationally visible rather than looking like successful graph builds.

AGENTS.md reference: AGENTS.md:L91-L91

Useful? React with 👍 / 👎.

Comment on lines +208 to +210
'omi-luna',
'omi-auto',
CHAT_AGENT_AUTO_LANE_ID,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Support managed aliases after gateway fallback

When gateway routing is disabled, or an Anthropic BYOK key deliberately changes gateway_mode to false, each newly accepted alias reaches _request, whose model allowlist still excludes omi-luna, omi-auto, and omi:auto:chat-agent; the request therefore returns HTTP 400 instead of taking the recorded Anthropic fallback. Empty or whitespace-only models now have the same inconsistency. Normalize these managed aliases to a supported direct model before _request, or keep them out of managed triage when that fallback cannot serve them.

Useful? React with 👍 / 👎.

'Prefer discovery_text with raw findings; backend extract builds the graph. nodes/edges remain accepted.'
],
latency: 'fast local',
latency: 'fast network',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Give network extraction a matching tool timeout

When knowledge-graph extraction takes between 30 and 60 seconds, the Windows relay's normal timeout returns an error at 30 seconds even though the newly added backend request is explicitly allowed to run for 60 seconds. The relay timeout also does not abort its controller, so the executor can subsequently persist the graph after the model was told the tool failed. Moving this tool from local to network latency must also move it to a timeout class that covers the request, or shorten and abort the underlying request at the relay deadline.

Useful? React with 👍 / 👎.

Does not write Firestore. Desktop onboarding/import should call this instead of inventing
memories via Anthropic Haiku chat completions, then persist via the normal memory write APIs.
"""
from utils.llm import memories as memories_llm

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move the memories LLM import to module scope

Every call to the new memories endpoint performs a function-local import of its LLM business-logic dependency, hiding the router-to-utils dependency from normal import-time validation and violating the backend's strict module hierarchy convention. Import utils.llm.memories at module scope, as required for backend serving code, so dependency and startup failures are detected before the first user request.

AGENTS.md reference: backend/AGENTS.md:L181-L187

Useful? React with 👍 / 👎.

@Git-on-my-level Git-on-my-level added needs-maintainer-review Needs a human maintainer to sign off before merge needs-tests PR introduces logic that should be covered by tests workflow-review Needs maintainer review for workflow, automation, hooks, or CI behavior desktop labels Aug 9, 2026
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks for pushing this toward a single backend-owned extraction path. I reviewed the diff and I’m leaving this for human maintainer review rather than approval because it changes backend LLM routing/API surface, desktop agent tool behavior, and the backend unit suite is currently red.

Code-specific notes:

  • backend/routers/knowledge_graph.py adds POST /v1/knowledge-graph/extract as a return-only endpoint with Firebase auth and knowledge_graph:extract rate limiting. That matches the intended “extract, don’t persist” boundary, but it is a new first-party user-memory/graph LLM surface and should get maintainer sign-off before merge.
  • backend/routers/memories.py adds POST /v1/memories/extract with extra=forbid, input length limits, and fail-closed 502 behavior when parsing/extraction fails. The endpoint looks bounded, but it moves memory-log extraction out of the desktop chat-completions path and into the managed memories feature, so product/data-handling review is still warranted.
  • backend/utils/llm/knowledge_graph.py splits extraction from persistence via extract_kg_from_text() and extraction_to_client_graph(). The empty-input and parse-failure behavior is conservative, and the legacy extract_knowledge_from_memory() path still persists via _persist_extraction(). One thing for maintainers to decide: the new client graph IDs are generated UUIDs per extraction, so desktop local writers should not assume id stability across repeated extraction of the same text.
  • backend/utils/llm/memories.py adds the SSOT memory-log prompt and routes it through get_llm('memories') with usage tracking. This is the right architectural direction, but it centralizes a user-profile extraction prompt that affects onboarding output quality and should be reviewed as product behavior, not only plumbing.
  • desktop/macos/Desktop/Sources/Onboarding/OnboardingMemoryLogImportService.swift removes the embedded Haiku prompt and calls APIClient.shared.extractMemoryLog(...) instead. That reduces duplicated prompt logic, but the PR test plan still leaves the real desktop import path unchecked.
  • desktop/macos/Desktop/Sources/Services/KnowledgeGraphToolSupport.swift and desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift make save_knowledge_graph accept discovery_text and perform a backend extraction before local save. This changes runtime tool behavior from fast local write to network-backed extraction, so user-facing latency/error handling needs maintainer acceptance.
  • desktop/macos/agent/src/runtime/omi-tool-manifest.ts, desktop/windows/src/main/agentKernel/omiToolManifest.ts, and the generated capability/fixture files change the instructions seen by desktop AI agents: nodes/edges are no longer required and agents are guided to send raw discovery_text. That is a real agent/tool-behavior change; the guidance is directionally safe because raw discovery text is routed to a backend SSOT extractor, but it may change how coding/review/chat agents use the graph-save tool and should be reviewed deliberately.
  • desktop/windows/src/main/agentKernel/productToolExecutors.ts mirrors the discovery_text backend extraction path and has a focused unit test in productToolExecutorsTierB.test.ts. Good coverage for the happy path; I would still want a failure-path assertion for backend extraction errors and/or malformed node payloads before relying on this for onboarding.
  • desktop/windows/src/renderer/src/lib/kgSynthesis.ts and desktop/windows/src/renderer/src/lib/memoryExtract.ts remove local Haiku/chat-completion synthesis and switch to /v1/knowledge-graph/extract and /v1/memories/extract. The mapping of backend node types into local Windows graph types is a product-schema choice (place/thing/conceptinterest) and should be accepted by a maintainer.
  • backend/route_policy_manifest.yaml, backend/utils/rate_limit_config.py, docs/api-reference/app-client-openapi.json, and the generated TypeScript clients add the API policy/rate-limit/schema surface for these endpoints. The policy entries correctly mark auth + fail-closed rate limiting, but this is still a new first-party API surface over user memory data.

Validation notes:

  • GitHub’s Backend unit suite is failing on this head. I inspected the CI log; several failures look like existing harness/config issues rather than clear defects in the changed production code, but the red suite still blocks approval.
  • I could not run the focused backend pytest locally in this review checkout because the local backend venv is missing dependencies (ModuleNotFoundError: No module named 'google').
  • The PR body’s desktop validation items are still unchecked for the actual onboarding memory-log import, save_knowledge_graph discovery_text, and Windows Settings local graph rebuild paths.

Recommended next step: keep this as a cohesive PR, but get maintainer sign-off on the desktop agent behavior change and run/record the real desktop paths once the backend suite is green.


Automated review generated by glm-5.2 for maintainer triage. A human maintainer should make the final product/merge decision.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

undivisible and others added 10 commits August 9, 2026 14:30
Add return-only POST /v1/connectors/synthesize backed by get_llm('memories') so the
calendar, gmail and notes prompts live in the backend instead of each desktop client
inventing memories/tasks/profile through Anthropic Haiku chat completions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Calendar, Gmail and Sticky Notes imports now post rows to POST /v1/connectors/synthesize
instead of building their own prompts and calling Anthropic Haiku via /v2/chat/completions.

Verified: pnpm vitest run (calendarExtract, gmailExtract, stickyNotesImport) — 12 passed;
tsc --noEmit -p tsconfig.web.json clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Calendar, Gmail and Apple Notes readers now post their formatted rows to
POST /v1/connectors/synthesize instead of building Haiku synthesis prompts and
parsing free-form JSON via AgentClient.

Verified: xcrun swift build -c debug --package-path Desktop — Build complete;
xctest -XCTest ConnectorSynthesisResponseTests — 2 tests, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add /v1/connectors to the app-client contract prefixes so the new synthesis
endpoint ships in the exported spec, and regenerate the Swift/TS clients.

Verified: xcrun swift build -c debug --package-path Desktop — Build complete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`topBarNewSinceRaw == 0` inside the didBecomeActive closure pushed the modifier
chain past the type-checker budget ("unable to type-check this expression in
reasonable time"), failing xcrun swift build -c debug on a clean tree. `.isZero`
removes the literal overload search with identical behavior.

Verified: xcrun swift build -c debug --package-path Desktop — Build complete
(fails on the same tree without this change).

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add return-only POST /v1/conversations/topic backed by get_llm('conv_structure')
so the provisional emoji + short title prompt lives in the backend, and route the
Windows fast-titling path there instead of Anthropic Haiku chat completions.

Verified: backend/.venv/bin/python -m pytest tests/unit/test_conversation_topic_helper.py
tests/unit/test_conversation_topic_route.py tests/unit/test_rate_limiting.py — 74 passed;
vitest conversationTopic.test.ts — 2 passed; tsc --noEmit -p tsconfig.web.json clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Connector synthesis and memory-log import now run through backend SSOT endpoints,
so no production path selects the Haiku synthesis model any more.

Verified: xcrun swift build -c debug --package-path Desktop --build-tests — Build
complete; xctest -XCTest ModelQoSTests — 16 tests, 0 failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0244d3567f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +122 to +123
with track_usage(uid, Features.MEMORIES):
response = get_llm('memories').invoke(prompt)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore the system-message boundary for connector data

When an attacker-controlled Gmail subject/snippet or a synced note contains prompt-like instructions, the connector rows and extraction contract are now sent together as one human message. The previous macOS and Windows connector paths supplied a separate system message; here the schema parser validates only the response shape, so injected but well-formed memories or tasks can be accepted and persisted by both clients. Pass the fixed contract as a system message and the connector rows as separately delimited human content.

Useful? React with 👍 / 👎.

Comment on lines +143 to +144
def _stage2_user_prompt(fresh_profile: str, past_profiles: List[str]) -> str:
past_section = "\n\n".join(f"--- Profile {i + 1} ---\n{text}" for i, text in enumerate(past_profiles))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve profile timestamps during consolidation

For macOS users whose historical profiles contain relative or time-sensitive facts such as “prepare the demo next Thursday,” this strips each record down to text and labels it only by ordinal position. The previous macOS consolidation prompt included each profile's generatedAt date, which allowed the model to interpret relative dates and remove expired commitments as the stage-two rules require; after this change those facts can be retained as current and injected into downstream pipelines. Carry the generation timestamp in the request contract and render it beside each historical profile.

Useful? React with 👍 / 👎.

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the follow-up. One of the previous blockers is fixed on this head: docs/api-reference/app-client-openapi.json now includes POST /v1/conversations/topic, and the current GitHub checks are green.

I’m still requesting changes for the remaining compatibility issue in the shipped desktop tool path:

  • desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift + desktop/macos/Desktop/Sources/Services/KnowledgeGraphToolSupport.swift: executeSaveKnowledgeGraph switches to backend extraction whenever the discovery_text key is present, before checking whether it trims to a non-empty value. KnowledgeGraphToolSupport.resolveDiscoveryText then returns Error: 'discovery_text' or 'nodes' is required for blank text. That means a legacy/compatibility call that still provides valid nodes/edges but also carries discovery_text: " " fails instead of saving the provided graph, even though the tool manifest says nodes/edges remain accepted for compatibility. Please only take the backend-extraction path when discovery_text.trimmingCharacters(...) is non-empty, otherwise fall back to the provided nodes/edges.

Other code-specific notes from this head:

  • desktop/windows/src/main/agentKernel/productToolExecutors.ts already uses the safer trimmed const discoveryText = ...trim() and only calls /v1/knowledge-graph/extract when that string is non-empty, so the Windows executor preserves the legacy fallback behavior.
  • backend/routers/conversations.py, backend/routers/knowledge_graph.py, backend/routers/memories.py, backend/routers/integrations.py, and backend/routers/users.py add return-only, Firebase-authenticated endpoints with explicit rate-limit policy entries in backend/route_policy_manifest.yaml; that direction looks coherent for moving desktop-owned prompt invention behind backend SSOT LLM routes.
  • backend/utils/llm/conversation_topic.py, backend/utils/llm/knowledge_graph.py, backend/utils/llm/memories.py, backend/utils/llm/connector_synthesis.py, and backend/utils/llm/ai_user_profile.py include parser/truncation guards and focused unit coverage for the helper and route seams.
  • .github/scripts/product_file_line_count_ratchet_baseline/*.json, backend/scripts/product_capability_synthetics.py, backend/testing/replay_harness_llm_gateway_fake_upstream/oracle.py, and backend/scripts/export_openapi.py have been updated to account for the new backend route and gateway lane behavior.

This PR also changes desktop AI-agent tool behavior: save_knowledge_graph now encourages raw discovery_text and may call the backend knowledge-graph extraction route before local persistence. That is a reasonable SSOT direction, but it changes agent latency/failure modes and the instructions coding/review agents see for this repo, so human maintainer sign-off is still warranted for the product/tool-behavior direction.

Reviewed by gpt-5.5 for the Omi maintainer automation; leaving this for human maintainer review because it changes backend-owned LLM extraction over memory/conversation/connector data and desktop agent tool behavior.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

undivisible and others added 11 commits August 10, 2026 06:16
… blank

executeSaveKnowledgeGraph switched to backend extraction on the mere presence of a
discovery_text key, so a compatibility call carrying valid nodes/edges plus a
whitespace-only discovery_text failed with "'discovery_text' or 'nodes' is required"
instead of saving the graph the tool supplied. Extraction now runs only for a
non-empty trimmed value, and an extract failure falls back to the provided nodes
(recorded as a degraded fallback) rather than discarding them.

Also drop KnowledgeGraphToolSupport.swift from memory-graph.yaml's covers: none of
that flow's steps reach the save_knowledge_graph tool path.

Verified: xcrun swift build -c debug --package-path Desktop --build-tests — Build
complete; xctest ConnectorSynthesisResponseTests / AIUserProfileSynthesisResponseTests
/ ModelQoSTests — 20 tests, 0 failures.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The five return-only LLM routes were synchronous handlers, so a slow provider held
Starlette's shared sync-route pool instead of the dedicated six-worker LLM lane and
could starve unrelated sync APIs. They also applied only an hourly rate limit: the
client paths they replaced went through /v2/chat/completions, which refuses a
trial-expired account before touching a provider.

Each handler is now async, dispatches its extractor with run_blocking(llm_executor,
...), and refuses a paywalled account with 402 before the model call. The
knowledge-graph route additionally passes strict_parse=True so a malformed model
response fails closed (502) instead of returning 200 with an empty graph that a
client cannot tell apart from a genuine "no entities" answer, and the memory-log
request cap now matches the helper's own 40k truncation budget instead of
advertising 100k and silently dropping the tail.

Verified: backend/.venv/bin/python -m pytest on the ten helper/route suites plus
test_rate_limiting.py and test_memories_create.py — 129 passed.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n input

Both helpers sent the extraction contract and the untrusted payload as one human
message. An imported ChatGPT/Claude log, a Gmail subject or snippet, and a synced
note are all attacker-reachable text, and the schema parser only validates the
response shape — so a well-formed injected memory or task would be accepted and
persisted by both clients. The client paths these replaced kept the contract in a
separate system message.

The contract is now a system message, the payload a separately delimited human
message, and both prompts state that the payload is data rather than instructions.

Verified: backend/.venv/bin/python -m pytest tests/unit/test_memories_extract_helper.py
tests/unit/test_connector_synthesis_helper.py — passing, with both tests asserting
the two-role split.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selecting omi-structured routed the request to omi:auto:chat-structured, but the
gateway path still hardcoded chat_agent in the request headers, the usage context,
the latency observation and the result record (including the chat-agent route id).
Every structured-lane attempt was therefore written to the accounting ledger and
reliability metrics as chat traffic, hiding the new lane's cost and failures. The
feature and route are now derived from the selected lane on both the streaming and
non-streaming paths.

Verified: backend/.venv/bin/python -m pytest tests/unit/test_desktop_chat.py — 59 passed.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r binding

The new extract/synthesis endpoints inherited OmiHTTPTransport's shared 30s request
timeout while Windows budgets 60s for the same routes, so a slow managed call was
cancelled client-side — worst on the profile route, which runs two sequential model
calls. post() now takes a per-request timeout and these four calls use 60s.

Profile synthesis also posted without an owner binding while carrying one account's
memories, messages and past profiles: a 401 retry after a mid-flight account switch
would have replayed them under the new account's token. It now pins an owner
authorization snapshot like the other extract methods.

Verified: xcrun swift build -c debug --package-path Desktop --build-tests — Build complete.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…action

discovery_text turned save_knowledge_graph into a network edge with a 60s backend
request, but the Windows executor ignored its ProductToolContext, so a relay
disconnect left the request running and still wrote the graph locally; and the tool
kept the 'normal' 30s relay deadline, which reports failure while that request is
still in flight. The executor now forwards ctx.signal and checks cancellation before
the local write, and both manifests move the tool to the 'long' timeout class.

Also drop the duplicated discovery_text prompt bullet (the capability doc already
states it) and regenerate the Swift tool surfaces, and make the Tier-B aliases
assertion actually exercise the mapping instead of matching on undefined.

Verified: vitest run src/main/agentKernel src/renderer/src/lib src/main/assistants —
2505 passed; tsc --noEmit on both Windows projects clean.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The connector transport caps each item at 1000 chars, so a Sticky Note line longer
than that lost its tail; long lines are now chunked instead of truncated. And when
the backend graph extract fails, Windows saves a deterministic-only graph and stamps
it fresh for 12 hours — a provider/correctness fallback that emitted only a
console.warn. It now writes the structured, loud fallback record the contract asks
for (the renderer has no recordFallback emitter; see billing.ts).

Verified: vitest run src/renderer/src/lib — passing; tsc --noEmit clean.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Making the handler async left get_user_name as a sync Firestore call on the event
loop, which the async-blocker gate rejects.

Verified: backend/.venv/bin/python -m pytest tests/unit/test_knowledge_graph_extract_route.py — 3 passed.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nges

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_38742660-b1b0-47e6-8f0c-78fee1b6dca2)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed54939785

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

)
log("AIUserProfileService: Stage 2 consolidation complete (\(finalText.count) chars)")
}
let synthesis = try await APIClient.shared.synthesizeAIUserProfile(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pin the owner before collecting profile sources

When account A starts generation and the app switches to B during fetchDataSources(), this call captures its authorization snapshot only after the asynchronous reads, so B can become the pinned owner while the payload still contains A's or mixed source lines and historical profiles; the response can then be persisted into B's local profile. Fresh evidence beyond the earlier comment is that the final implementation still calls synthesizeAIUserProfile without the originating expectedOwnerId or snapshot after source collection. Capture one owner snapshot before any reads and thread it through collection, synthesis, and persistence.

AGENTS.md reference: AGENTS.md:L89-L89

Useful? React with 👍 / 👎.

// still stamped fresh, so this must not look like a successful build. The renderer
// has no recordFallback emitter (see billing.ts), so this is the same structured,
// loud console record the fallback contract asks for.
console.error('[kg] fallback', {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit fallback telemetry for degraded graph builds

When graph extraction fails, this branch persists the deterministic-only graph and stamps it fresh for 12 hours, but console.error does not emit the operational fallback_triggered event; the same Windows renderer already uses trackEvent('fallback_triggered', ...) for degraded branches. Fresh evidence beyond the prior comment is that the revised branch only changed to a structured console record rather than an actual telemetry emitter, so backend extraction outages remain invisible to fallback monitoring.

AGENTS.md reference: AGENTS.md:L91-L91

Useful? React with 👍 / 👎.

Comment on lines +114 to +115
existing = [m.strip() for m in (existing_memories or []) if m.strip()][:MAX_EXISTING]
existing_block = "\n".join(f"- {m}" for m in existing) if existing else "(none)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound existing memories before constructing the prompt

When a Windows Gmail or Sticky Notes import supplies long existing memories, this keeps up to 200 strings at unlimited length and inserts them verbatim into the sole LLM prompt. Memory content itself has no model-level length bound, and the Windows transport truncates only the list count, so a few large manual/imported memories can exceed the provider context and make connector synthesis return 502 even though source items are capped. Apply a per-memory character limit here, as the memory-log extraction route already does.

Useful? React with 👍 / 👎.

Comment on lines +193 to +196
if consolidated:
profile_text = consolidated
else:
logger.warning("AI user profile stage 2 returned empty content for uid=%s; keeping stage 1", uid)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject an empty profile consolidation result

When historical profiles exist and the second model call succeeds at the transport layer but returns empty content, this silently falls back to the fresh stage-one profile and the route returns 200. Both desktop clients then store and sync that incomplete profile, temporarily dropping stable facts that existed only in historical profiles; the previous Windows transport treated empty content as an error and kept the prior profile instead. Return None so the route responds 502, or explicitly record this correctness-degrading fallback before allowing clients to overwrite the consolidated profile.

AGENTS.md reference: AGENTS.md:L91-L91

Useful? React with 👍 / 👎.


def cleaned(self) -> "ProfileSources":
def clean(lines: List[str]) -> List[str]:
return [line.strip()[:MAX_LINE_CHARS] for line in lines if line.strip()][:MAX_LINES_PER_SOURCE]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve long profile source lines

When a memory, conversation overview, or AI chat message exceeds 1,000 characters, this silently discards the remainder of that source item before synthesis. Both previous desktop implementations joined the complete fetched strings into the stage-one prompt, and the new request schema permits these longer values, so durable facts appearing later in an otherwise valid long message can no longer enter the generated profile. Chunk long entries within the source-count budget or enforce a total prompt budget without dropping each line's tail.

Useful? React with 👍 / 👎.

The entitlement gate imported utils.subscription at module scope, which pulls
database.users in at import time and broke the v3 router isolation test's minimal
dependency graph. The import now sits beside the LLM helper's deferred import, with
the reason recorded, and the isolation stub gains the llm_executor the router now
dispatches through.

Verified: backend/.venv/bin/python -m pytest tests/unit/test_v3_real_router_default_off_f4.py
tests/unit/test_memories_extract_route.py — 19 passed; the four other new route suites
— 12 passed; tests/unit/test_import_isolation.py tests/unit/test_agent_tools_isolation.py — 5 passed.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Git-on-my-level
Git-on-my-level dismissed stale reviews from themself August 9, 2026 23:58

Resolved on current head: OpenAPI contract includes the conversation topic route and macOS save_knowledge_graph now falls back to provided nodes/edges when discovery_text is blank.

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks for the update. I re-checked the current head and the two prior blocking review concerns are resolved, so I dismissed my stale CHANGES_REQUESTED reviews for those older commits:

  • docs/api-reference/app-client-openapi.json / backend/scripts/export_openapi.py: POST /v1/conversations/topic is now represented/excluded consistently for the first-party app contract path, and the current Public Developer API contract check is passing.
  • desktop/macos/Desktop/Sources/Providers/ChatToolExecutor.swift + desktop/macos/Desktop/Sources/Services/KnowledgeGraphToolSupport.swift: executeSaveKnowledgeGraph now trims discovery_text first and only calls KnowledgeGraphToolSupport.resolveDiscoveryText when the trimmed value is non-empty, so blank discovery_text falls back to the compatibility nodes/edges path instead of failing.

Current code-specific observations:

  • backend/routers/conversations.py, backend/routers/knowledge_graph.py, backend/routers/memories.py, backend/routers/integrations.py, and backend/routers/users.py add return-only, Firebase-authenticated LLM extraction/synthesis endpoints with rate-limit policy entries in backend/route_policy_manifest.yaml. That looks coherent for moving desktop prompt invention behind backend SSOT routes.
  • backend/utils/llm/conversation_topic.py, backend/utils/llm/knowledge_graph.py, backend/utils/llm/memories.py, backend/utils/llm/connector_synthesis.py, and backend/utils/llm/ai_user_profile.py keep the model calls behind typed helper seams with parser/error guards and matching helper/route unit coverage.
  • desktop/windows/src/main/agentKernel/productToolExecutors.ts already uses the same non-empty trimmed discovery_text gate before calling /v1/knowledge-graph/extract, so Windows preserves the legacy explicit-node save path too.
  • backend/routers/desktop_chat.py now separates omi:auto:chat-structured from the chat-agent lane for gateway payload/model/accounting, and the related backend/tests/unit/test_desktop_chat.py cases cover the lane selection and feature attribution.
  • .github/scripts/product_file_line_count_ratchet_baseline/*.json, backend/scripts/product_capability_synthetics.py, and backend/testing/replay_harness_llm_gateway_fake_upstream/oracle.py were updated for the new route/gateway lane behavior rather than leaving the deterministic checks stale.

I am not requesting changes on this head. The remaining red Backend unit suite appears to be test-isolation/stub fallout rather than a production-path regression: the failed files import routers under intentionally mocked database / utils.executors / utils.subscription modules, and the new route imports expose missing stubs such as database.user_usage, utils.subscription, and llm_executor. That still needs the CI/test fixtures fixed or rerun green before merge, so I’m leaving needs-tests in place.

This PR also changes desktop AI-agent tool behavior: save_knowledge_graph now encourages raw discovery_text and can call backend knowledge-graph extraction before local persistence. The direction looks reasonable, but it changes agent latency/failure modes and backend-owned LLM processing over memory/conversation/connector data, so I’m leaving this for human maintainer review rather than formal approval.

Reviewed by gpt-5.5 for the Omi maintainer automation.


by AI on behalf of David — maintainer sign-off is needed here for the new backend-owned LLM extraction APIs and desktop agent tool behavior change.

@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_f9048c7e-8cc9-410f-86e1-28e973963a10)

@undivisible

Copy link
Copy Markdown
Collaborator Author

Fixed the blocker plus the P1s from the bot reviews on this head.

BlockerexecuteSaveKnowledgeGraph now takes the backend-extract path only when discovery_text trims to non-empty, and an extract failure falls back to supplied nodes/edges (recorded as a degraded fallback) instead of discarding them.

Also fixed

  • All five managed extraction routes: async + run_blocking(llm_executor, ...), and a 402 for a paywalled account before the provider call.
  • KG extract passes strict_parse=True, so a malformed model response is a 502 rather than a 200 with an empty graph.
  • Extraction prompts split into a system contract + a separate human message, so imported logs / Gmail metadata / notes can't rewrite the rules.
  • Structured-lane traffic is accounted to chat_structured and its own route id instead of chat_agent.
  • macOS managed synthesis calls get a 60s timeout; profile synthesis pins an owner authorization snapshot.
  • Windows save_knowledge_graph forwards ctx.signal and moves to the long relay timeout class; long Sticky Note lines are chunked rather than truncated.

Not done, deliberately — deterministic KG entity IDs and the kgSynthesis node-type/summary contract are real but are a change to the extraction contract itself; they belong in their own PR rather than widening this one.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1aaaaff72a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Mirrors the lane + endpoint localAgent.ts uses for its agent loop. This is not a
// chat turn, so it selects the managed structured lane explicitly rather than the
// conversational chat-agent lane (backend routers/desktop_chat.py _managed_lane_id).
const AGENT_MODEL = 'omi-structured'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep structured planning usable without gateway routing

When gateway routing is disabled or an Anthropic BYOK key forces the direct path, this new omi-structured model reaches desktop_chat._request, whose _MODEL_ROUTES allowlist does not contain that alias, so the Windows action planner receives HTTP 400 and does not invoke its fallback. Fresh evidence beyond the earlier managed-alias comment is that the final diff now switches both the planner and local-agent callers to this newly added structured alias; normalize it to a supported direct model when the structured gateway lane is unavailable.

Useful? React with 👍 / 👎.

…tion

Same defect as the memories router: the entitlement gate imported utils.subscription
at module scope, which pulls database.user_usage in at import time and broke the
minimal dependency graph that test_conversation_events_bounds.py and
test_conversation_search_date_validation.py build. The import now sits beside the LLM
helper's deferred import with the reason recorded.

Verified: BACKEND_UNIT_TEST_FILE_LIST covering test_conversation_events_bounds.py,
test_conversation_search_date_validation.py and test_conversation_topic_route.py via
backend/test.sh — 9 + 23 + 3 passed.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbb6faffb9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

their own prompts and inventing memories via Anthropic Haiku chat completions, then
persist through the normal memory/task write APIs.
"""
from utils.llm import connector_synthesis

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move the remaining LLM imports to module scope

Beyond the already-commented memories endpoint, the new connector, AI-profile, and conversation-topic handlers still import their LLM helpers inside the request function. If one of these dependencies is missing or develops an import-time failure, the backend can pass startup validation and only return 500 when a user first invokes that route; move these dependencies to module scope and adjust the isolation tests instead of hiding the serving dependency.

AGENTS.md reference: backend/AGENTS.md:L179-L187

Useful? React with 👍 / 👎.

uid,
body.text,
user_name=user_name,
load_existing_from_db=body.include_existing,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fetch existing graph nodes on the database executor

When a caller sets include_existing=true, this dispatches the entire helper to the six-worker LLM pool, where extract_kg_from_text first performs the blocking Firestore get_knowledge_nodes read before invoking the provider. Concurrent slow database reads can therefore occupy every LLM worker and delay unrelated model work; load the nodes with run_blocking(db_executor, ...) before this call and pass them as existing_nodes.

AGENTS.md reference: backend/AGENTS.md:L289-L292

Useful? React with 👍 / 👎.

Comment on lines +375 to +378
let memoryStrings = synthesis.memories.filter {
!$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
let profileSummary = parsed["profile"] as? String ?? ""
let profileSummary = synthesis.profile

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist note-derived tasks instead of discarding them

When the shared notes prompt classifies an explicit commitment into synthesis.tasks as instructed, this consumer reads only memories and profile, so the commitment never reaches the task store. The Windows Sticky Notes consumer drops the same field and can even report no-new-memories when a task is the only result; unlike the previous notes prompt, which had no task output and asked for plans among memories, the new contract therefore silently loses actionable note content.

Useful? React with 👍 / 👎.


class ExtractKnowledgeGraphRequest(BaseModel):
text: str = Field(..., min_length=1, max_length=100_000)
user_name: Optional[str] = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound the caller-supplied graph user name

When an authenticated caller supplies a very large user_name, this field bypasses the request's 100,000-character text limit and is interpolated into the extraction prompt multiple times. A multi-megabyte value can consume substantial memory, occupy an LLM worker, and produce an oversized provider request even though the actual extraction text is bounded; apply a small max_length consistent with user-name fields elsewhere.

Useful? React with 👍 / 👎.

@Git-on-my-level Git-on-my-level removed the needs-tests PR introduces logic that should be covered by tests label Aug 10, 2026
Base automatically changed from backend-ssot-openrouter to main August 10, 2026 05:59
@undivisible
undivisible merged commit bbc0158 into main Aug 10, 2026
19 checks passed
@undivisible
undivisible deleted the backend-ssot-kg-chat branch August 10, 2026 06:03
undivisible added a commit that referenced this pull request Aug 10, 2026
…sts (#11360)

## Summary
Reverts the two OpenRouter PRs: #11283 (managed product text →
OpenRouter Luna) and #11284 (dynamic OpenRouter model catalog).

**No `OPENROUTER_API_KEY` is provisioned.** As merged, every managed
product-text feature — chat, memories, knowledge graph, conversation
processing, goals, notifications, wrapped — resolves to an OpenRouter
route and would fail at the provider, and
`llm_gateway/routers/health.py` requires that key before reporting
ready, so the gateway would never pass readiness.

Routing config returns exactly to its pre-#11283 state (`git diff`
against the commit before that merge is empty for `model_config.py`,
`llm_gateway/config/`, `health.py` and `clients.py`).

## Kept
The SSOT work from #11286 and #11325 stays:
`/v1/knowledge-graph/extract`, `/v1/memories/extract`,
`/v1/connectors/synthesize`, `/v1/conversations/topic`,
`/v1/users/ai-profile/synthesize` and deterministic KG ids. Those route
through `get_llm(feature)`, so they follow whatever provider
`model_config` names — now direct OpenAI/Anthropic again — and keep
working.

## Product invariants affected
- INV-AGENT-*
- INV-CHAT-1
- INV-MEM-1

## Failure class (fixes)

Failure-Class: none

## Test plan
- [x] `backend/test.sh` over the 41 gateway/qos/openrouter/SSOT-endpoint
test files — all pass file-by-file (one pre-existing fast-unit CPU-time
guard trip on `test_llm_gateway_deploy_contract.py`, 13/13 assertions
pass, unrelated to this diff)
- [x] Routing config byte-identical to pre-#11283 for `model_config.py`,
`llm_gateway/config/`, `health.py`, `clients.py`
- [ ] Dev backend deploy reports ready without `OPENROUTER_API_KEY`


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/BasedHardware/omi/pull/11360?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Touches core LLM routing, gateway provider execution, readiness, and
BYOK behavior for all managed text features; wrong config would break
chat and background LLM workloads at scale.
> 
> **Overview**
> **Reverts managed product text routing from OpenRouter back to direct
providers** so the stack can run without `OPENROUTER_API_KEY` and
gateway `/ready` no longer blocks on that credential.
> 
> `model_config` and gateway **generated route overrides** again send
most features to **direct OpenAI** (`gpt-5.6-luna` / `gpt-5-nano`),
**Gemini** for former flash-lite workloads, **Anthropic** for
`chat_agent`, and **OpenRouter** only for `wrapped_analysis`. Inventory,
route artifacts, and cost cards are aligned with that map; OpenRouter
Luna/nano rate cards are removed.
> 
> The gateway **executor** drops OpenRouter-specific request shaping: no
BYOK vendor remapping on OpenRouter routes, no OpenRouter completion
clamp, and GPT-5.6 sanitization applies only to `openai` provider refs.
**Health** reports `managed_chat_provider: openai` and requires
`OPENAI_API_KEY` instead of OpenRouter.
> 
> **Deleted** the dynamic OpenRouter model catalog and shared
vendor-prefix helpers; synthetics/replay harnesses register **`openai`**
fakes directly again. QoS and gateway unit/integration tests are updated
to match the pre–OpenRouter managed-text expectations.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3b0aa79. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop needs-maintainer-review Needs a human maintainer to sign off before merge workflow-review Needs maintainer review for workflow, automation, hooks, or CI behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants