feat:[ENG-2075] add ByteRover context tree as write fallback#396
feat:[ENG-2075] add ByteRover context tree as write fallback#396danhdoan merged 6 commits intoproj/swarm-memoryfrom
Conversation
- Change selectWriteTarget() to return null instead of throwing when no writable providers are available - Add optional ICurateService to SwarmCoordinator constructor - Add fallbackToByterover() that routes content to the curation pipeline (snippets) when no external provider can accept the write - Add fallback?: boolean to SwarmStoreResult type - Update CLI curate output to show fallback message - 5 new tests: fallback with/without curateService, fallback on curate failure, no fallback for explicit --provider, normal write without fallback flag
Resolved conflicts in swarm-coordinator.ts: - Constructor: keep both curateService (ENG-2075) and resultCacheTtlMs (ENG-2073) - Private methods: keep fallbackToByterover (ENG-2075) alongside buildCacheKey and evictIfOverSize (ENG-2073)
|
Claude finished @cuongdo-byterover's task in 2m 24s —— View job PR Review: feat:[ENG-2075] add ByteRover context tree as write fallbackScore: 3.5 / 5 — Solid feature implementation with clean dependency injection and good test coverage. A few semantic inconsistencies and one silent-failure path need attention before merging.
Issuessuggestion (correctness): Stale The JSDoc on - * @throws Error if no writable+healthy provider is available
+ * @returns null if no writable+healthy provider is availableissue (semantic inconsistency): In A caller doing Suggestion: omit the field when no fallback was attempted, matching the established pattern: return {
error: 'No writable providers available and curate service not configured...',
- fallback: false,
id: '',
latencyMs: Date.now() - start,
provider: '',
success: false,
}issue (correctness): Silent failure when
const firstApplied = result.applied[0]
return {
fallback: true,
id: firstApplied?.path ?? 'context-tree',
latencyMs: Date.now() - start,
provider: 'byterover',
success: firstApplied?.status === 'success', // false when undefined
}If Suggestion: detect the empty case explicitly: const firstApplied = result.applied[0]
if (!firstApplied) {
return {
error: 'Curate service returned no applied operations',
fallback: true,
id: '',
latencyMs: Date.now() - start,
provider: 'byterover',
success: false,
}
}
return {
fallback: true,
id: firstApplied.path,
latencyMs: Date.now() - start,
provider: 'byterover',
success: firstApplied.status === 'success',
}suggestion (minor):
const curateService = flags.provider ? undefined : new CurateService(workingDirectory)
const coordinator = new SwarmCoordinator(providers, config, curateService)nitpick: Missing test for The 5 new tests cover the main paths well. The gap is when it('fallback returns failure when curateService returns empty applied', async () => {
const mockCurate = { curate: sinon.stub().resolves({ applied: [], summary: {...} }), detectDomains: sinon.stub() }
const coordinator = new SwarmCoordinator([obsidian], config, mockCurate)
const result = await coordinator.store({content: 'test'})
expect(result.success).to.be.false
expect(result.error).to.include('no applied operations')
})What's good
|
…teService The fallback now shells out to `brv curate --detach` — same path as the CLI command. This goes through the daemon + LLM agent for proper domain detection, topic routing, and content structuring, instead of manually constructing CurateOperation objects with hardcoded paths. Also addresses PR review comments: - Fix stale @throws JSDoc in selectWriteTarget (now returns null) - Remove fallback: false from no-curateService path (use undefined) - Remove CurateService dependency from coordinator and CLI
|
Claude finished @cuongdo-byterover's task in 2m 59s —— View job PR Review: feat:[ENG-2075] add ByteRover context tree as write fallbackScore: 3 / 5 — The approach pivot from
Issuesissue (correctness):
id: result.stdout.trim(),
The execFile('brv', ['curate', '--detach', '--format', 'json', request.content], ...)
// ...
const parsed = JSON.parse(result.stdout)
return {
fallback: true,
id: parsed.data?.logId ?? parsed.data?.taskId ?? '',
latencyMs: Date.now() - start,
provider: 'byterover',
success: parsed.success === true,
}issue (CLAUDE.md violation): Unit test does not mock
// The fallback calls `brv curate --detach` via execFile.
// We can't easily mock execFile here, so we verify the result shape
// when the fallback is triggered (it will fail because brv daemon
// may not be running in test, but the fallback flag should be set).CLAUDE.md is unambiguous: "Unit tests must run fast and run completely in memory. Proper stubbing and mocking must be implemented." A test that spawns a real
import * as childProcess from 'node:child_process'
// ...
let execFileStub: sinon.SinonStub
beforeEach(() => {
execFileStub = sinon.stub(childProcess, 'execFile')
})
afterEach(() => { execFileStub.restore() })
it('falls back to brv curate when no writable providers', async () => {
execFileStub.callsFake((_cmd, _args, _opts, cb) => {
cb(null, '{"success":true,"data":{"logId":"log-abc","taskId":"task-xyz","status":"queued"}}', '')
})
// ...
expect(result.fallback).to.be.true
expect(result.id).to.equal('log-abc')
})suggestion (robustness): Large content will exceed OS
execFile('brv', ['curate', '--detach', request.content], ...)Linux's The safest approach is to write content to a temp file and pass the path via import {writeFile, mkdtemp, rm} from 'node:fs/promises'
import {tmpdir} from 'node:os'
import {join} from 'node:path'
const dir = await mkdtemp(join(tmpdir(), 'brv-fallback-'))
const tmpFile = join(dir, 'content.txt')
try {
await writeFile(tmpFile, request.content, 'utf8')
execFile('brv', ['curate', '--detach', '--format', 'json', '--files', tmpFile, 'Fallback content'], ...)
} finally {
await rm(dir, {recursive: true, force: true})
}nitpick: PR description still references the removed The most recent commit ( What's good
|
- Use --format json for brv curate subprocess, parse logId from JSON instead of capturing human-readable stdout - Extract execBrvCurate as a standalone function, inject via constructor (CurateFallbackFn) so tests can mock without ES module stubbing issues - Add proper unit tests with sinon stubs for both success and failure fallback paths - Fix stale @throws JSDoc in selectWriteTarget (now @returns null)
|
Claude finished @cuongdo-byterover's task in 2m 21s —— View job PR Review: feat:[ENG-2075] add ByteRover context tree as write fallbackScore: 3.5 / 5 — The pivot to
Issuesissue (lint failure): Imports split by a function declaration in
Move all imports to the top, then place // --- top of file ---
import {execFile} from 'node:child_process'
import type {QueryRequest} from '../../core/domain/swarm/types.js'
import type {IMemoryProvider} from '../../core/interfaces/i-memory-provider.js'
import type {
ISwarmCoordinator,
ProviderInfo,
ProviderQueryMeta,
SwarmQueryResult,
SwarmStoreRequest,
SwarmStoreResult,
SwarmSummary,
} from '../../core/interfaces/i-swarm-coordinator.js'
import type {SwarmConfig} from './config/swarm-config-schema.js'
import {SwarmGraph} from './swarm-graph.js'
// ... rest of imports ...
export function execBrvCurate(...) { ... }issue (correctness): The resolved type is Add export function execBrvCurate(content: string): Promise<{
data?: {logId?: string; taskId?: string}
error?: string
success?: boolean
}> { ... }
// in fallbackToByterover, success path:
return {
error: parsed.success === true ? undefined : (parsed.error ?? 'brv curate returned success: false'),
fallback: true,
id: parsed.data?.logId ?? parsed.data?.taskId ?? '',
latencyMs: Date.now() - start,
provider: 'byterover',
success: parsed.success === true,
}suggestion (robustness): Large
import {mkdtemp, rm, writeFile} from 'node:fs/promises'
import {tmpdir} from 'node:os'
import {join} from 'node:path'
const dir = await mkdtemp(join(tmpdir(), 'brv-fallback-'))
const tmpFile = join(dir, 'content.txt')
try {
await writeFile(tmpFile, content, 'utf8')
// pass --files tmpFile with a short title arg
execFile('brv', ['curate', '--detach', '--format', 'json', '--files', tmpFile, 'fallback'], ...)
} finally {
await rm(dir, {recursive: true, force: true})
}nitpick: Tests inject the mock via the nitpick: CLI output has trailing "as " when
Condition it on const idPart = result.id ? ` as ${result.id}` : ''
this.log(`Stored to ${result.provider} (fallback — no external providers available)${idPart}`)What's good
|
- Move imports to top of file, place execBrvCurate after (fix lint) - Add error field to BrvCurateResult type, propagate parsed.error into SwarmStoreResult when brv curate returns success: false - Remove export from execBrvCurate (only used internally, tests inject via CurateFallbackFn constructor param) - Handle empty id in CLI output (no trailing "as ") Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use util.promisify(execFile) instead of manual Promise wrapper - Export BrvCurateResult type so CurateFallbackFn is complete for external consumers - Fix stale selectWriteTarget docstring (no longer says callers must pre-filter providers)
|
Claude finished @cuongdo-byterover's task in 2m 16s —— View job PR Review: feat:[ENG-2075] add ByteRover context tree as write fallbackScore: 4 / 5 — All three hard issues and both nitpicks from the previous review are resolved. Two minor new items introduced: a
Issuesissue (CLAUDE.md violation):
const err = error as {message: string; stderr?: string}CLAUDE.md says: "Avoid } catch (error) {
const stderr =
error instanceof Error && 'stderr' in error && typeof (error as {stderr: unknown}).stderr === 'string'
? (error as {stderr: string}).stderr.trim()
: undefined
throw new Error(stderr || (error instanceof Error ? error.message : String(error)))
}nitpick: Missing test for The four fallback tests cover: success path ( it('fallback propagates error message when brv curate returns success: false', async () => {
const mockCurate = sinon.stub().resolves({
error: 'queue full',
success: false,
})
const obsidian = createMockProvider('obsidian', 'obsidian', [])
const config = createMinimalConfig()
const coordinator = new SwarmCoordinator([obsidian], config, mockCurate)
const result = await coordinator.store({content: 'test'})
expect(result.success).to.be.false
expect(result.fallback).to.be.true
expect(result.error).to.equal('queue full')
})suggestion (robustness): Large
What's good
|
Summary
Type of change
Scope (select all touched areas)
Linked issues
Root cause (bug fixes only, otherwise write
N/A)Test plan
User-visible changes
List user-visible changes (including defaults, config, or CLI output).
If none, write
None.Evidence
Attach at least one:
Checklist
npm test)npm run lint)npm run typecheck)npm run build)mainRisks and mitigations
List real risks for this PR. If none, write
None.