* feat: add cross-integration entity references * fix(web): anchor entity reference menu to composer * fix: keep Kandev tasks under @ mentions * fix: address entity reference review findings * test(e2e): use option role for prompt menu * test(e2e): update passthrough mention roles * fix: preserve queued references and paginate Azure PRs
6.1 KiB
ADR-2026-07-21-work-item-reference-search: Backend-Normalized Work-Item References
Status: accepted Date: 2026-07-21 Area: backend, frontend, protocol
Context
Kandev's chat composer currently builds @ suggestions in the browser from unrelated local sources. Work-item search spans seven provider families with different credentials, workspace scopes, query languages, pagination, identities, and failure modes. A selected item must also remain unambiguous after its title or repository changes and while a message waits in the durable queue.
Decision
Kandev owns work-item typeahead behind a backend provider registry in internal/mentions. Every provider implements a plain-text, workspace-scoped search boundary and translates that query into its private API dialect. Registry and aggregate service depend only on hand-defined, versioned normalized DTOs plus a provider descriptor; they do not import native Jira/GitHub/etc. models, plugin wire models, structpb, or switch exhaustively on native provider IDs. The aggregate endpoint runs providers concurrently with bounded work and timeouts, returns deterministic provider/kind groups, and treats provider failures as partial results with safe status codes.
Providers return candidates with provider-local immutable identity plus non-secret connection scope. The registry injects its registered provider identity, validates candidate fields, and constructs the canonical versioned ref; an adapter cannot spoof another provider. Each (provider, kind) registration also owns a ReferenceAuthorizer, used both to filter search destinations and to authorize message submission against the trusted conversation workspace. This keeps configured-origin and scope checks provider-owned without a central provider switch. Jira and GitHub adapters retain upstream immutable IDs that their current projections drop; providers without a single global ID use a documented provider-native composite. URLs and titles are presentation snapshots, not identity.
The frontend uses a separate # entityReference atom. @ remains the context-attachment channel for files, prompts, and plans. New task discovery moves to #, while legacy @task nodes remain compatible.
Selected references serialize into both portable Markdown links in visible message content and typed entity_references in existing message/queue metadata JSON. A neutral internal/entityrefs leaf owns structural normalization, canonical identity, and deduplication; provider authorization remains in internal/mentions. Metadata drives chip rendering and sanitized agent context; Markdown is the durable fallback. No new reference table or synchronized cross-provider index is introduced.
Built-in integrations register adapters through this host seam. provider and kind are validated, additive strings with generic presentation fallbacks, not closed enums. A future PluginProviderBridge can therefore enumerate active plugins with an explicit mention-provider contribution and adapt a typed Kandev-to-plugin Plugin.SearchMentions RPC to the same MentionProvider interface. This is not part of the plugin-to-Kandev Host data API and does not reuse api_read. Defining the manifest contribution, permission/grant, additive RPC, lifecycle/error mapping, and workspace authorization is a separate public-contract decision following ADR 0043; this feature does not add them. Older plugins that return gRPC Unimplemented will map to provider-unavailable without failing other sources.
Consequences
- Workspace isolation, provider query escaping, timeouts, error classification, and identity normalization live in one backend boundary instead of being duplicated in React.
- Adding a built-in provider requires an adapter and conformance tests but does not change the composer contract.
- Migrating a built-in integration to a plugin replaces its adapter registration with a plugin bridge while preserving registry-issued provider/source IDs (or explicit aliases), normalized reference identity, message metadata, and frontend behavior. New plugin-only providers use reserved namespaced IDs.
- Jira and GitHub search models must preserve upstream immutable IDs. Azure DevOps and Sentry adapters need bounded project/repository or instance/organization discovery because their current browse APIs require extra scope.
- Typeahead consumes external API quota. Client debounce/cancellation, server caps, and provider timeouts are correctness requirements. Any provider/client cache must be short-lived and keyed by workspace plus provider connection scope so it cannot cross authorization boundaries.
- Partial results are expected. Cross-provider scores are not compared; results stay grouped and deterministic.
- Message and queue metadata already provide restart durability, avoiding a migration and reference lifecycle table. Stale targets keep their display snapshot but are not guaranteed to open successfully.
- Plugins cannot contribute
#results until a future contribution, permission, and Kandev-to-plugin RPC contract is designed, but current core code must not add coupling that would require a second composer/search contract.
Alternatives Considered
Fan out from the frontend
Rejected. It would expose provider query dialects and failure semantics to React, duplicate workspace/config checks, complicate cancellation, and make every new provider a composer change.
Reuse the existing @ mention pipeline
Rejected. @ items attach local context and have different serialization/selection semantics. Mixing external durable entities into that union would preserve the current overloaded menu and make migration harder.
Store only plain text or Markdown links
Rejected. Mutable labels and URLs cannot reliably identify a task or work item for agent tools, queue edits, history recall, or sent-message chip rendering.
Create a normalized reference table or local search index
Rejected for this release. References do not own target lifecycle, and synchronizing every provider would add migrations, webhook/polling consistency, retention, and stale-index behavior before typeahead needs them.