Skip to content

Commit 03b8739

Browse files
Fix/eng 2535 (#592)
* fix: [ENG-2535] surface daemon version in register ack + drift indicators Companion to the transport-client fix. Wires the daemon's own version through the register ack so MCP and TUI clients can render version-drift state without an extra round-trip, and adds a drift footer to MCP tool responses + a header tag in the TUI when the running daemon was started by a different brv build. - ConnectionCoordinator: accept daemonVersion option, surface it in the client:register ack response. Threaded through TransportHandlers so brv-server can pass the package.json-read version it already holds. - mcp-server: log a one-line stderr drift notice on connect and after every reconnect, when daemon and MCP versions differ. - brv-query-tool / brv-curate-tool: append a drift footer to text responses when client.getDaemonVersion() differs from the MCP version. - drift-footer: pure helper covered by 4 unit tests. - transport-store: add daemonVersion field + setter; transport-initializer pushes it on initial connect and on every reconnect. - header: render `[outdated, daemon vX.Y.Z]` in colors.warning when the TUI's version differs from the running daemon. - Bulk-update transport client mocks across 27 test files to include the new getDaemonVersion() interface method. - repro.sh: split verdict into BUG REPRODUCED vs FIX VERIFIED so the script doubles as a regression check. * chore: [ENG-2543] bump brv-transport-client to 1.1.0 Pin @campfirein/brv-transport-client to the released 1.1.0 tag now that the asymmetric daemon-version gate from ENG-2540 is published, replacing the branch ref that was used while the fix was in review. * fix: [ENG-2541] render daemon-version drift indicator inline in header banner Previously the drift token was rendered on a separate Box below the logo, which broke the header's single-line layout. Fold it into the banner next to the version, with padEnd recomputed so the trailing slashes still fill the row. * fix: [ENG-2541] address PR review nits on drift surfaces mcp-server.ts: flip versionsAreEquivalent args to (client, daemon) so all three drift surfaces (header, footer, log) use a consistent ordering. The function is symmetric so this is purely a readability change for anyone grepping the call sites. header.tsx: replace Boolean(daemonVersion) with daemonVersion !== undefined so TypeScript control-flow analysis narrows daemonVersion to string in the right operand of the && expression.
1 parent bd912b9 commit 03b8739

45 files changed

Lines changed: 328 additions & 25 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 16 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@ai-sdk/vercel": "^1.0.33",
3434
"@ai-sdk/xai": "^2.0.57",
3535
"@anthropic-ai/sdk": "^0.70.1",
36-
"@campfirein/brv-transport-client": "github:campfirein/brv-transport-client#1.0.0",
36+
"@campfirein/brv-transport-client": "github:campfirein/brv-transport-client#1.1.0",
3737
"@campfirein/byterover-packages": "github:campfirein/byterover-packages#1.0.2",
3838
"@google/genai": "^1.29.0",
3939
"@inkjs/ui": "^2.0.0",
@@ -77,8 +77,8 @@
7777
"react": "^19.2.1",
7878
"react-diff-viewer-continued": "^4.2.0",
7979
"react-dom": "^19.2.1",
80-
"react-reconciler": "0.33.0",
8180
"react-markdown": "^10.1.0",
81+
"react-reconciler": "0.33.0",
8282
"react-router-dom": "^7.13.0",
8383
"react-syntax-highlighter": "^16.1.1",
8484
"remark-frontmatter": "^5.0.0",

src/server/infra/daemon/brv-server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,10 @@ async function main(): Promise<void> {
404404
const transportHandlers = new TransportHandlers({
405405
agentPool,
406406
clientManager,
407+
// The version we read at startup gets relayed in the client:register ack
408+
// so peer clients (TUI / MCP) can render drift indicators without an
409+
// extra round-trip.
410+
daemonVersion: version,
407411
// Resolves the project's review-disabled flag once at task-create. The result
408412
// is stamped onto TaskInfo + TaskExecute so daemon hooks (CurateLogHandler) and
409413
// the agent process (curate-tool backups, dream review entries) all observe a

src/server/infra/mcp/mcp-server.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createDaemonReconnector,
66
type DaemonReconnectorHandle,
77
type ITransportClient,
8+
versionsAreEquivalent,
89
} from '@campfirein/brv-transport-client'
910
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
1011
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'
@@ -87,12 +88,14 @@ export class ByteRoverMcpServer {
8788
() => this.client,
8889
() => this.getWorkingDirectory(),
8990
getStartupProjectContext,
91+
config.version,
9092
)
9193
registerBrvCurateTool(
9294
this.server,
9395
() => this.client,
9496
() => this.getWorkingDirectory(),
9597
getStartupProjectContext,
98+
config.version,
9699
)
97100
}
98101

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

126130
// Auto-reconnect on disconnect (shared logic from brv-transport-client)
127131
this.reconnectorHandle = createDaemonReconnector(result.client, {
128132
connectOptions: this.connectOptions,
129133
onReconnected: (newClient: ITransportClient) => {
130134
this.client = newClient
131135
this.log(`Reconnected successfully! Client ID: ${newClient.getClientId()}`)
136+
this.logDaemonVersionDrift(newClient.getDaemonVersion?.())
132137
this.sendAgentName()
133138
},
134139
onStateChange: (state: ConnectionState) => {
@@ -208,6 +213,19 @@ export class ByteRoverMcpServer {
208213
process.stderr.write(`[brv-mcp] ${msg}\n`)
209214
}
210215

216+
/**
217+
* Logs a one-line drift notice when the running daemon's version differs
218+
* from this MCP's. Helps users notice an out-of-sync IDE without forcing
219+
* a reconnect — the protocol is backward-compatible across the gap.
220+
*/
221+
private logDaemonVersionDrift(daemonVersion: string | undefined): void {
222+
if (daemonVersion && !versionsAreEquivalent(this.config.version, daemonVersion)) {
223+
this.log(
224+
`connected to daemon ${daemonVersion}; this MCP is ${this.config.version} (backward-compatible protocol)`,
225+
)
226+
}
227+
}
228+
211229
/**
212230
* Sends the cached agent name to the daemon (fire-and-forget).
213231
* Called after MCP initialize handshake and on Socket.IO reconnection.

src/server/infra/mcp/tools/brv-curate-tool.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {randomUUID} from 'node:crypto'
66
import {z} from 'zod'
77

88
import {TransportTaskEventNames} from '../../../core/domain/transport/schemas.js'
9+
import {appendDriftFooter} from './drift-footer.js'
910
import {associateProjectWithRetry, type McpStartupProjectContext, resolveMcpTaskContext} from './mcp-project-context.js'
1011
import {resolveClientCwd} from './resolve-client-cwd.js'
1112
import {cwdField} from './shared-schema.js'
@@ -47,6 +48,7 @@ export function registerBrvCurateTool(
4748
getClient: () => ITransportClient | undefined,
4849
getWorkingDirectory: () => string | undefined,
4950
getStartupProjectContext: () => McpStartupProjectContext | undefined,
51+
clientVersion: string,
5052
): void {
5153
server.registerTool(
5254
'brv-curate',
@@ -121,10 +123,11 @@ export function registerBrvCurateTool(
121123
const logId = ack?.logId
122124
const modeDescription = hasFolder ? 'folder pack' : 'curation'
123125
const logSuffix = logId ? `, logId: ${logId}` : ''
126+
const queuedMessage = `✓ Context queued for ${modeDescription} (taskId: ${taskId}${logSuffix}). The curation will be processed asynchronously.`
124127
return {
125128
content: [
126129
{
127-
text: `✓ Context queued for ${modeDescription} (taskId: ${taskId}${logSuffix}). The curation will be processed asynchronously.`,
130+
text: appendDriftFooter(queuedMessage, clientVersion, client.getDaemonVersion?.()),
128131
type: 'text' as const,
129132
},
130133
],

src/server/infra/mcp/tools/brv-query-tool.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {randomUUID} from 'node:crypto'
66
import {z} from 'zod'
77

88
import {TransportTaskEventNames} from '../../../core/domain/transport/schemas.js'
9+
import {appendDriftFooter} from './drift-footer.js'
910
import {associateProjectWithRetry, type McpStartupProjectContext, resolveMcpTaskContext} from './mcp-project-context.js'
1011
import {resolveClientCwd} from './resolve-client-cwd.js'
1112
import {cwdField} from './shared-schema.js'
@@ -27,6 +28,7 @@ export function registerBrvQueryTool(
2728
getClient: () => ITransportClient | undefined,
2829
getWorkingDirectory: () => string | undefined,
2930
getStartupProjectContext: () => McpStartupProjectContext | undefined,
31+
clientVersion: string,
3032
): void {
3133
server.registerTool(
3234
'brv-query',
@@ -85,7 +87,7 @@ export function registerBrvQueryTool(
8587
const result = await resultPromise
8688

8789
return {
88-
content: [{text: result, type: 'text' as const}],
90+
content: [{text: appendDriftFooter(result, clientVersion, client.getDaemonVersion?.()), type: 'text' as const}],
8991
}
9092
} catch (error) {
9193
const message = error instanceof Error ? error.message : String(error)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {versionsAreEquivalent} from '@campfirein/brv-transport-client'
2+
3+
/**
4+
* Appends a version-drift footer to MCP tool text responses when the MCP
5+
* client and the running daemon are at different versions.
6+
*
7+
* Returns the body unchanged when versions match or when the daemon hasn't
8+
* reported its version yet (pre-fix daemon during a rolling upgrade).
9+
*/
10+
export function appendDriftFooter(body: string, clientVersion: string, daemonVersion?: string): string {
11+
if (!daemonVersion || versionsAreEquivalent(clientVersion, daemonVersion)) {
12+
return body
13+
}
14+
15+
return (
16+
body +
17+
`\n\n---\nNote: this brv MCP is at ${clientVersion} while the running daemon is at ${daemonVersion}. ` +
18+
`The protocol is backward-compatible. Restart your IDE to align versions.`
19+
)
20+
}

src/server/infra/process/connection-coordinator.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ import {broadcastToProjectRoom} from './broadcast-utils.js'
4141
type ConnectionCoordinatorOptions = {
4242
agentPool?: IAgentPool
4343
clientManager?: IClientManager
44+
/**
45+
* Daemon version surfaced in `client:register` ack. Lets clients render
46+
* drift indicators without a separate round-trip. Optional so older
47+
* deployments still build.
48+
*/
49+
daemonVersion?: string
4450
projectRegistry?: IProjectRegistry
4551
projectRouter?: IProjectRouter
4652
taskRouter: TaskRouter
@@ -55,6 +61,7 @@ export class ConnectionCoordinator {
5561
private agentClients: Map<string, string> = new Map()
5662
private readonly agentPool: IAgentPool | undefined
5763
private readonly clientManager: IClientManager | undefined
64+
private readonly daemonVersion: string | undefined
5865
private readonly projectRegistry: IProjectRegistry | undefined
5966
private readonly projectRouter: IProjectRouter | undefined
6067
private readonly taskRouter: TaskRouter
@@ -64,6 +71,7 @@ export class ConnectionCoordinator {
6471
this.transport = options.transport
6572
this.agentPool = options.agentPool
6673
this.clientManager = options.clientManager
74+
this.daemonVersion = options.daemonVersion
6775
this.projectRouter = options.projectRouter
6876
this.projectRegistry = options.projectRegistry
6977
this.taskRouter = options.taskRouter
@@ -252,7 +260,7 @@ export class ConnectionCoordinator {
252260
private handleClientRegister(
253261
clientId: string,
254262
data: {clientType: ClientType; projectPath?: string},
255-
): {error?: string; success: boolean} {
263+
): {daemonVersion?: string; error?: string; success: boolean} {
256264
if (!this.clientManager) {
257265
return {error: 'ClientManager not available', success: false}
258266
}
@@ -278,6 +286,10 @@ export class ConnectionCoordinator {
278286
this.addToProjectRoom(clientId, data.projectPath)
279287
}
280288

289+
if (this.daemonVersion) {
290+
return {daemonVersion: this.daemonVersion, success: true}
291+
}
292+
281293
return {success: true}
282294
}
283295

@@ -468,10 +480,10 @@ export class ConnectionCoordinator {
468480
}
469481

470482
private setupClientLifecycleHandlers(): void {
471-
this.transport.onRequest<{clientType: ClientType; projectPath?: string}, {error?: string; success: boolean}>(
472-
TransportClientEventNames.REGISTER,
473-
(data, clientId) => this.handleClientRegister(clientId, data),
474-
)
483+
this.transport.onRequest<
484+
{clientType: ClientType; projectPath?: string},
485+
{daemonVersion?: string; error?: string; success: boolean}
486+
>(TransportClientEventNames.REGISTER, (data, clientId) => this.handleClientRegister(clientId, data))
475487

476488
this.transport.onRequest<{projectPath: string}, {error?: string; success: boolean}>(
477489
TransportClientEventNames.ASSOCIATE_PROJECT,

src/server/infra/process/transport-handlers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export type {TaskInfo} from './types.js'
4444
type TransportHandlersOptions = {
4545
agentPool?: IAgentPool
4646
clientManager?: IClientManager
47+
/**
48+
* Daemon's CLI version (read from package.json at startup). Surfaced in the
49+
* `client:register` ack so clients can render version-drift indicators.
50+
*/
51+
daemonVersion?: string
4752
/** Resolves project's review-disabled flag at task-create. Snapshotted once into TaskInfo + TaskExecute. */
4853
isReviewDisabled?: IsReviewDisabledResolver
4954
/** Lifecycle hooks for task events (e.g. CurateLogHandler). */
@@ -81,6 +86,7 @@ export class TransportHandlers {
8186
this.connectionCoordinator = new ConnectionCoordinator({
8287
agentPool: options.agentPool,
8388
clientManager: options.clientManager,
89+
daemonVersion: options.daemonVersion,
8490
projectRegistry: options.projectRegistry,
8591
projectRouter: options.projectRouter,
8692
taskRouter: this.taskRouter,

src/tui/components/header.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* - Queue stats (pending/processing)
88
*/
99

10+
import {versionsAreEquivalent} from '@campfirein/brv-transport-client'
1011
import {Box} from 'ink'
1112
import React from 'react'
1213

@@ -19,11 +20,16 @@ interface HeaderProps {
1920

2021
export const Header: React.FC<HeaderProps> = ({compact}) => {
2122
const version = useTransportStore((s) => s.version)
23+
const daemonVersion = useTransportStore((s) => s.daemonVersion)
24+
25+
// Drift indicator surfaces when this brv build connects to a daemon spawned
26+
// by a different build. Rendered inline by Logo so the banner stays a single
27+
// line; hidden when versions match or the daemon is too old to advertise.
28+
const isOutdated = daemonVersion !== undefined && !versionsAreEquivalent(version, daemonVersion)
2229

2330
return (
2431
<Box flexDirection="column" marginBottom={1} width="100%">
25-
{/* Logo */}
26-
<Logo compact={compact} version={version} />
32+
<Logo compact={compact} driftDaemonVersion={isOutdated ? daemonVersion : undefined} version={version} />
2733
</Box>
2834
)
2935
}

0 commit comments

Comments
 (0)