Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@ai-sdk/vercel": "^1.0.33",
"@ai-sdk/xai": "^2.0.57",
"@anthropic-ai/sdk": "^0.70.1",
"@campfirein/brv-transport-client": "github:campfirein/brv-transport-client#1.0.0",
"@campfirein/brv-transport-client": "github:campfirein/brv-transport-client#1.1.0",
"@campfirein/byterover-packages": "github:campfirein/byterover-packages#1.0.2",
"@google/genai": "^1.29.0",
"@inkjs/ui": "^2.0.0",
Expand Down Expand Up @@ -77,8 +77,8 @@
"react": "^19.2.1",
"react-diff-viewer-continued": "^4.2.0",
"react-dom": "^19.2.1",
"react-reconciler": "0.33.0",
"react-markdown": "^10.1.0",
"react-reconciler": "0.33.0",
"react-router-dom": "^7.13.0",
"react-syntax-highlighter": "^16.1.1",
"remark-frontmatter": "^5.0.0",
Expand Down
4 changes: 4 additions & 0 deletions src/server/infra/daemon/brv-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,10 @@ async function main(): Promise<void> {
const transportHandlers = new TransportHandlers({
agentPool,
clientManager,
// The version we read at startup gets relayed in the client:register ack
// so peer clients (TUI / MCP) can render drift indicators without an
// extra round-trip.
daemonVersion: version,
// Resolves the project's review-disabled flag once at task-create. The result
// is stamped onto TaskInfo + TaskExecute so daemon hooks (CurateLogHandler) and
// the agent process (curate-tool backups, dream review entries) all observe a
Expand Down
18 changes: 18 additions & 0 deletions src/server/infra/mcp/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createDaemonReconnector,
type DaemonReconnectorHandle,
type ITransportClient,
versionsAreEquivalent,
} from '@campfirein/brv-transport-client'
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'
Expand Down Expand Up @@ -87,12 +88,14 @@ export class ByteRoverMcpServer {
() => this.client,
() => this.getWorkingDirectory(),
getStartupProjectContext,
config.version,
)
registerBrvCurateTool(
this.server,
() => this.client,
() => this.getWorkingDirectory(),
getStartupProjectContext,
config.version,
)
}

Expand Down Expand Up @@ -122,13 +125,15 @@ export class ByteRoverMcpServer {
this.log(`Connected to brv instance at ${result.projectRoot}`)
this.log(`Client ID: ${result.client.getClientId()}`)
this.log(`Initial connection state: ${result.client.getState()}`)
this.logDaemonVersionDrift(result.client.getDaemonVersion?.())

// Auto-reconnect on disconnect (shared logic from brv-transport-client)
this.reconnectorHandle = createDaemonReconnector(result.client, {
connectOptions: this.connectOptions,
onReconnected: (newClient: ITransportClient) => {
this.client = newClient
this.log(`Reconnected successfully! Client ID: ${newClient.getClientId()}`)
this.logDaemonVersionDrift(newClient.getDaemonVersion?.())
this.sendAgentName()
},
onStateChange: (state: ConnectionState) => {
Expand Down Expand Up @@ -208,6 +213,19 @@ export class ByteRoverMcpServer {
process.stderr.write(`[brv-mcp] ${msg}\n`)
}

/**
* Logs a one-line drift notice when the running daemon's version differs
* from this MCP's. Helps users notice an out-of-sync IDE without forcing
* a reconnect — the protocol is backward-compatible across the gap.
*/
private logDaemonVersionDrift(daemonVersion: string | undefined): void {
if (daemonVersion && !versionsAreEquivalent(this.config.version, daemonVersion)) {
this.log(
`connected to daemon ${daemonVersion}; this MCP is ${this.config.version} (backward-compatible protocol)`,
)
}
}

/**
* Sends the cached agent name to the daemon (fire-and-forget).
* Called after MCP initialize handshake and on Socket.IO reconnection.
Expand Down
5 changes: 4 additions & 1 deletion src/server/infra/mcp/tools/brv-curate-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {randomUUID} from 'node:crypto'
import {z} from 'zod'

import {TransportTaskEventNames} from '../../../core/domain/transport/schemas.js'
import {appendDriftFooter} from './drift-footer.js'
import {associateProjectWithRetry, type McpStartupProjectContext, resolveMcpTaskContext} from './mcp-project-context.js'
import {resolveClientCwd} from './resolve-client-cwd.js'
import {cwdField} from './shared-schema.js'
Expand Down Expand Up @@ -47,6 +48,7 @@ export function registerBrvCurateTool(
getClient: () => ITransportClient | undefined,
getWorkingDirectory: () => string | undefined,
getStartupProjectContext: () => McpStartupProjectContext | undefined,
clientVersion: string,
): void {
server.registerTool(
'brv-curate',
Expand Down Expand Up @@ -121,10 +123,11 @@ export function registerBrvCurateTool(
const logId = ack?.logId
const modeDescription = hasFolder ? 'folder pack' : 'curation'
const logSuffix = logId ? `, logId: ${logId}` : ''
const queuedMessage = `✓ Context queued for ${modeDescription} (taskId: ${taskId}${logSuffix}). The curation will be processed asynchronously.`
return {
content: [
{
text: `✓ Context queued for ${modeDescription} (taskId: ${taskId}${logSuffix}). The curation will be processed asynchronously.`,
text: appendDriftFooter(queuedMessage, clientVersion, client.getDaemonVersion?.()),
type: 'text' as const,
},
],
Expand Down
4 changes: 3 additions & 1 deletion src/server/infra/mcp/tools/brv-query-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {randomUUID} from 'node:crypto'
import {z} from 'zod'

import {TransportTaskEventNames} from '../../../core/domain/transport/schemas.js'
import {appendDriftFooter} from './drift-footer.js'
import {associateProjectWithRetry, type McpStartupProjectContext, resolveMcpTaskContext} from './mcp-project-context.js'
import {resolveClientCwd} from './resolve-client-cwd.js'
import {cwdField} from './shared-schema.js'
Expand All @@ -27,6 +28,7 @@ export function registerBrvQueryTool(
getClient: () => ITransportClient | undefined,
getWorkingDirectory: () => string | undefined,
getStartupProjectContext: () => McpStartupProjectContext | undefined,
clientVersion: string,
): void {
server.registerTool(
'brv-query',
Expand Down Expand Up @@ -85,7 +87,7 @@ export function registerBrvQueryTool(
const result = await resultPromise

return {
content: [{text: result, type: 'text' as const}],
content: [{text: appendDriftFooter(result, clientVersion, client.getDaemonVersion?.()), type: 'text' as const}],
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
Expand Down
20 changes: 20 additions & 0 deletions src/server/infra/mcp/tools/drift-footer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {versionsAreEquivalent} from '@campfirein/brv-transport-client'

/**
* Appends a version-drift footer to MCP tool text responses when the MCP
* client and the running daemon are at different versions.
*
* Returns the body unchanged when versions match or when the daemon hasn't
* reported its version yet (pre-fix daemon during a rolling upgrade).
*/
export function appendDriftFooter(body: string, clientVersion: string, daemonVersion?: string): string {
if (!daemonVersion || versionsAreEquivalent(clientVersion, daemonVersion)) {
return body
}

return (
body +
`\n\n---\nNote: this brv MCP is at ${clientVersion} while the running daemon is at ${daemonVersion}. ` +
`The protocol is backward-compatible. Restart your IDE to align versions.`
)
}
22 changes: 17 additions & 5 deletions src/server/infra/process/connection-coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ import {broadcastToProjectRoom} from './broadcast-utils.js'
type ConnectionCoordinatorOptions = {
agentPool?: IAgentPool
clientManager?: IClientManager
/**
* Daemon version surfaced in `client:register` ack. Lets clients render
* drift indicators without a separate round-trip. Optional so older
* deployments still build.
*/
daemonVersion?: string
projectRegistry?: IProjectRegistry
projectRouter?: IProjectRouter
taskRouter: TaskRouter
Expand All @@ -55,6 +61,7 @@ export class ConnectionCoordinator {
private agentClients: Map<string, string> = new Map()
private readonly agentPool: IAgentPool | undefined
private readonly clientManager: IClientManager | undefined
private readonly daemonVersion: string | undefined
private readonly projectRegistry: IProjectRegistry | undefined
private readonly projectRouter: IProjectRouter | undefined
private readonly taskRouter: TaskRouter
Expand All @@ -64,6 +71,7 @@ export class ConnectionCoordinator {
this.transport = options.transport
this.agentPool = options.agentPool
this.clientManager = options.clientManager
this.daemonVersion = options.daemonVersion
this.projectRouter = options.projectRouter
this.projectRegistry = options.projectRegistry
this.taskRouter = options.taskRouter
Expand Down Expand Up @@ -252,7 +260,7 @@ export class ConnectionCoordinator {
private handleClientRegister(
clientId: string,
data: {clientType: ClientType; projectPath?: string},
): {error?: string; success: boolean} {
): {daemonVersion?: string; error?: string; success: boolean} {
if (!this.clientManager) {
return {error: 'ClientManager not available', success: false}
}
Expand All @@ -278,6 +286,10 @@ export class ConnectionCoordinator {
this.addToProjectRoom(clientId, data.projectPath)
}

if (this.daemonVersion) {
Comment thread
bao-byterover marked this conversation as resolved.
return {daemonVersion: this.daemonVersion, success: true}
}

return {success: true}
Comment thread
bao-byterover marked this conversation as resolved.
}

Expand Down Expand Up @@ -468,10 +480,10 @@ export class ConnectionCoordinator {
}

private setupClientLifecycleHandlers(): void {
this.transport.onRequest<{clientType: ClientType; projectPath?: string}, {error?: string; success: boolean}>(
TransportClientEventNames.REGISTER,
(data, clientId) => this.handleClientRegister(clientId, data),
)
this.transport.onRequest<
{clientType: ClientType; projectPath?: string},
{daemonVersion?: string; error?: string; success: boolean}
>(TransportClientEventNames.REGISTER, (data, clientId) => this.handleClientRegister(clientId, data))

this.transport.onRequest<{projectPath: string}, {error?: string; success: boolean}>(
TransportClientEventNames.ASSOCIATE_PROJECT,
Expand Down
6 changes: 6 additions & 0 deletions src/server/infra/process/transport-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export type {TaskInfo} from './types.js'
type TransportHandlersOptions = {
agentPool?: IAgentPool
clientManager?: IClientManager
/**
* Daemon's CLI version (read from package.json at startup). Surfaced in the
* `client:register` ack so clients can render version-drift indicators.
*/
daemonVersion?: string
/** Resolves project's review-disabled flag at task-create. Snapshotted once into TaskInfo + TaskExecute. */
isReviewDisabled?: IsReviewDisabledResolver
/** Lifecycle hooks for task events (e.g. CurateLogHandler). */
Expand Down Expand Up @@ -81,6 +86,7 @@ export class TransportHandlers {
this.connectionCoordinator = new ConnectionCoordinator({
agentPool: options.agentPool,
clientManager: options.clientManager,
daemonVersion: options.daemonVersion,
projectRegistry: options.projectRegistry,
projectRouter: options.projectRouter,
taskRouter: this.taskRouter,
Expand Down
10 changes: 8 additions & 2 deletions src/tui/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* - Queue stats (pending/processing)
*/

import {versionsAreEquivalent} from '@campfirein/brv-transport-client'
import {Box} from 'ink'
import React from 'react'

Expand All @@ -19,11 +20,16 @@ interface HeaderProps {

export const Header: React.FC<HeaderProps> = ({compact}) => {
const version = useTransportStore((s) => s.version)
const daemonVersion = useTransportStore((s) => s.daemonVersion)

// Drift indicator surfaces when this brv build connects to a daemon spawned
// by a different build. Rendered inline by Logo so the banner stays a single
// line; hidden when versions match or the daemon is too old to advertise.
const isOutdated = daemonVersion !== undefined && !versionsAreEquivalent(version, daemonVersion)

return (
<Box flexDirection="column" marginBottom={1} width="100%">
{/* Logo */}
<Logo compact={compact} version={version} />
<Logo compact={compact} driftDaemonVersion={isOutdated ? daemonVersion : undefined} version={version} />
</Box>
)
}
Loading
Loading