Repository navigation
馃尶 feat: Report Lane Git Branch And Head On Command Results #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
51818fd
347b061
9c429bf
435348d
48fd181
26c6b85
34aa914
b561845
b01ebe8
484f535
8223f42
3eacbb8
d4bc46e
1dcf887
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,7 +41,8 @@ import { RuntimeWorkspaceCommandSandbox } from './workspace-runtime.js'; | |
| import { NativeProcessWorkspaceCommandSandbox } from './native-process.js'; | ||
| import { NativeWorkspaceCommandPool } from './native-pool.js'; | ||
| import { GitWorktreeWorkspaceTools, internalWorkspaceId } from './workspace-instances.js'; | ||
| import { LINKED_WORKTREE_DIRECTORY, LinkedWorktreeWorkspaceTools } from './linked-worktrees.js'; | ||
| import { LaneGitWorkspaceTools } from './lane-git.js'; | ||
| import { LINKED_WORKTREE_DIRECTORY, LinkedWorktreeWorkspaceTools, verifyLinkedWorktree } from './linked-worktrees.js'; | ||
| import { GitWorktreeManager } from './worktrees.js'; | ||
| import { | ||
| WorktreeRetirementScheduler, | ||
|
|
@@ -1312,6 +1313,26 @@ async function run( | |
| }); | ||
| workspaceTools = linkedWorktreeTools; | ||
| } | ||
| // Set from each registration response; stays false against a Code API that does not offer lane_git. | ||
| let laneGitNegotiated = false; | ||
| if (workspaceTools?.capabilities.operations.includes('execute_command')) { | ||
| workspaceTools = new LaneGitWorkspaceTools({ | ||
| delegate: workspaceTools, | ||
| isEnabled: () => laneGitNegotiated, | ||
| async resolveRoot(request, signal) { | ||
| const source = roots.find((root) => root.id === request.workspaceId); | ||
| if (!source) return undefined; | ||
| if (request.workspaceInstanceId != null) { | ||
| return await conversationWorktrees?.plannedRoot(request.workspaceId, request.workspaceInstanceId); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a request specifies both Useful? React with 馃憤聽/ 馃憥.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could not reproduce this. A request with both workspaceInstanceId and worktree is rejected before anything runs: resolveLane throws INVALID_REQUEST 'Linked worktree lanes are not available inside conversation worktrees' (linked-worktrees.ts:270). I ran LinkedWorktreeWorkspaceTools with both set and got that rejection with zero delegate calls, so no command result exists and no laneGit is produced for that combination. If nested lanes are meant to be supported later, the resolver would need to resolve the nested root then; I left it as is. |
||
| } | ||
| if (request.worktree != null) { | ||
| signal?.throwIfAborted(); | ||
| return (await verifyLinkedWorktree(source.root, request.worktree, source.identity)).root; | ||
|
berry-13 marked this conversation as resolved.
Outdated
|
||
| } | ||
| return source.root; | ||
|
berry-13 marked this conversation as resolved.
Outdated
|
||
| }, | ||
| }); | ||
| } | ||
| const retirementSources = roots.filter((root) => root.writable); | ||
| const debugLogs = | ||
| process.env.LIBRECHAT_CODE_LOG_LEVEL?.trim().toLowerCase() === 'debug'; | ||
|
|
@@ -1604,8 +1625,10 @@ async function run( | |
| }); | ||
| } | ||
| : undefined, | ||
| onRegistered: fileRelaySupervisor | ||
| ? async registration => { | ||
| onRegistered: async registration => { | ||
| laneGitNegotiated = | ||
| registration.supportedWorkspaceCommandResultFeatures?.includes('lane_git') === true; | ||
|
berry-13 marked this conversation as resolved.
Outdated
|
||
| if (!fileRelaySupervisor) return; | ||
| if ( | ||
| registration.registrationGeneration == null || | ||
| !Number.isSafeInteger( | ||
|
|
@@ -1621,8 +1644,7 @@ async function run( | |
| registration.registrationGeneration, | ||
| controller.signal, | ||
| ); | ||
| } | ||
| : undefined, | ||
| }, | ||
| onError: error => { | ||
| const message = | ||
| error instanceof Error | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,334 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { execFile } from 'node:child_process'; | ||
| import { mkdir, mkdtemp, realpath, rm } from 'node:fs/promises'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
| import test from 'node:test'; | ||
| import { promisify } from 'node:util'; | ||
|
|
||
| import { LaneGitWorkspaceTools, readLaneGit } from './lane-git.js'; | ||
| import { isValidBridgeWorkspaceToolCapabilities, isWorkspaceLaneGit, isWorkspaceToolResult } from './protocol.js'; | ||
| import { BridgeWorker } from './worker.js'; | ||
|
|
||
| import type { TestContext } from 'node:test'; | ||
| import type { WorkspaceExecuteCommandRequest, WorkspaceToolRequest } from './protocol.js'; | ||
|
|
||
| const exec = promisify(execFile); | ||
| const incarnationId = 'incarnation-00000001'; | ||
| const identity = ['-c', 'user.name=Test', '-c', 'user.email=test@example.com']; | ||
|
|
||
| async function scratch(t: TestContext): Promise<string> { | ||
| const root = await realpath(await mkdtemp(join(tmpdir(), 'lane-git-'))); | ||
| t.after(() => rm(root, { recursive: true, force: true })); | ||
| return root; | ||
| } | ||
|
|
||
| async function repo(root: string, name: string, commit = true): Promise<string> { | ||
| const directory = join(root, name); | ||
| await mkdir(directory, { recursive: true }); | ||
| await exec('git', ['init', '--initial-branch=main', directory]); | ||
| if (commit) await exec('git', ['-C', directory, ...identity, 'commit', '--allow-empty', '-m', 'initial']); | ||
| return directory; | ||
| } | ||
|
|
||
| async function sha(directory: string): Promise<string> { | ||
| return (await exec('git', ['-C', directory, 'rev-parse', 'HEAD'])).stdout.trim(); | ||
| } | ||
|
|
||
| test('isolated worktree lane reports its own branch and head', async (t) => { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source'); | ||
| const lane = join(root, 'lane'); | ||
| await exec('git', ['-C', source, 'worktree', 'add', '-b', 'librechat/conversation-abcd1234-0123', lane]); | ||
| assert.deepEqual(await readLaneGit(lane), { branch: 'librechat/conversation-abcd1234-0123', head: await sha(lane) }); | ||
| assert.equal((await readLaneGit(source))?.branch, 'main'); | ||
| }); | ||
|
|
||
| test('source checkout reports whatever is checked out', async (t) => { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source'); | ||
| await exec('git', ['-C', source, 'checkout', '-b', 'feature/pr-header']); | ||
| assert.deepEqual(await readLaneGit(source), { branch: 'feature/pr-header', head: await sha(source) }); | ||
| }); | ||
|
|
||
| test('a branch renamed by the agent shows up on the next read', async (t) => { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source'); | ||
| assert.equal((await readLaneGit(source))?.branch, 'main'); | ||
| await exec('git', ['-C', source, 'branch', '-m', 'renamed-by-agent']); | ||
| assert.equal((await readLaneGit(source))?.branch, 'renamed-by-agent'); | ||
| }); | ||
|
|
||
| test('detached HEAD reports a null branch and the commit', async (t) => { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source'); | ||
| await exec('git', ['-C', source, 'checkout', '--detach']); | ||
| assert.deepEqual(await readLaneGit(source), { branch: null, head: await sha(source) }); | ||
| }); | ||
|
|
||
| test('a repository without commits reports the branch and a null head', async (t) => { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source', false); | ||
| assert.deepEqual(await readLaneGit(source), { branch: 'main', head: null }); | ||
| }); | ||
|
|
||
| test('a branch name beyond the bound reports null and never forwards it', async (t) => { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source'); | ||
| const long = ['a'.repeat(100), 'b'.repeat(100), 'c'.repeat(100)].join('/'); | ||
| await exec('git', ['-C', source, 'checkout', '-b', long]); | ||
| assert.deepEqual(await readLaneGit(source), { branch: null, head: await sha(source) }); | ||
| }); | ||
|
|
||
| test('a directory that is not a repository reports nothing', async (t) => { | ||
| const root = await scratch(t); | ||
| assert.equal(await readLaneGit(root), undefined); | ||
| }); | ||
|
|
||
| test('lane Git state never carries paths, remotes or credentials', async (t) => { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source'); | ||
| await exec('git', ['-C', source, 'remote', 'add', 'origin', 'https://user:secret@github.com/example/app.git']); | ||
| const state = await readLaneGit(source); | ||
| assert.deepEqual(Object.keys(state ?? {}).sort(), ['branch', 'head']); | ||
| const wire = JSON.stringify(state); | ||
| assert.equal(wire.includes(root), false); | ||
| assert.equal(wire.includes('secret'), false); | ||
| assert.equal(wire.includes('github.com'), false); | ||
| }); | ||
|
|
||
| test('wire validator accepts only bounded branch and head', () => { | ||
| const head = 'a'.repeat(40); | ||
| assert.equal(isWorkspaceLaneGit({ branch: 'main', head }), true); | ||
| assert.equal(isWorkspaceLaneGit({ branch: null, head: null }), true); | ||
| assert.equal(isWorkspaceLaneGit({ branch: 'main', head: 'b'.repeat(64) }), true); | ||
| for (const bad of [ | ||
| { branch: 'x'.repeat(257), head }, | ||
| { branch: 'bad\nname', head }, | ||
| { branch: '', head }, | ||
| { branch: 'main', head: 'A'.repeat(40) }, | ||
| { branch: 'main', head: 'a'.repeat(41) }, | ||
| { branch: 'main' }, | ||
| { branch: 'main', head, path: '/srv/repo' }, | ||
| { branch: undefined, head }, | ||
| null, | ||
| [], | ||
| ]) { | ||
| assert.equal(isWorkspaceLaneGit(bad), false, JSON.stringify(bad)); | ||
| } | ||
| }); | ||
|
|
||
| const commandRequest: WorkspaceExecuteCommandRequest = { | ||
| protocolVersion: 1, | ||
| operation: 'execute_command', | ||
| workspaceId: 'primary', | ||
| command: 'git checkout -b agent-branch', | ||
| }; | ||
| const commandResult = { | ||
| protocolVersion: 1 as const, | ||
| operation: 'execute_command' as const, | ||
| workspaceId: 'primary', | ||
| exitCode: 0, | ||
| stdout: '', | ||
| stderr: '', | ||
| truncated: false, | ||
| timedOut: false, | ||
| }; | ||
|
|
||
| function delegate(onRun?: () => Promise<void>) { | ||
| return { | ||
| capabilities: { | ||
| protocolVersion: 1 as const, | ||
| operations: ['read_file' as const, 'execute_command' as const], | ||
| workspaces: [{ id: 'primary' }], | ||
| }, | ||
| async execute(request: WorkspaceToolRequest) { | ||
| await onRun?.(); | ||
| return request.operation === 'execute_command' | ||
| ? commandResult | ||
| : { protocolVersion: 1 as const, operation: 'read_file' as const, workspaceId: 'primary', path: 'a', content: '', startLine: 1, endLine: 1, truncated: false }; | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| test('a finished command refreshes the lane state, including a branch the command created', async (t) => { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source'); | ||
| const tools = new LaneGitWorkspaceTools({ | ||
| delegate: delegate(async () => { | ||
| await exec('git', ['-C', source, 'checkout', '-b', 'agent-branch']); | ||
| }), | ||
| resolveRoot: async () => source, | ||
| }); | ||
| const result = await tools.execute(commandRequest); | ||
| assert.deepEqual('laneGit' in result && result.laneGit, { branch: 'agent-branch', head: await sha(source) }); | ||
| assert.equal(isWorkspaceToolResult(commandRequest, result), true); | ||
| }); | ||
|
|
||
| test('file operations do not read Git', async () => { | ||
| let resolved = 0; | ||
| const tools = new LaneGitWorkspaceTools({ | ||
| delegate: delegate(), | ||
| resolveRoot: async () => { | ||
| resolved++; | ||
| return undefined; | ||
| }, | ||
| }); | ||
| const result = await tools.execute({ protocolVersion: 1, operation: 'read_file', workspaceId: 'primary', path: 'a' }); | ||
| assert.equal('laneGit' in result, false); | ||
| assert.equal(resolved, 0); | ||
| }); | ||
|
|
||
| test('an unreadable lane leaves the command result untouched', async (t) => { | ||
| const root = await scratch(t); | ||
| for (const resolveRoot of [async () => root, async () => undefined, async () => Promise.reject(new Error('gone'))]) { | ||
| const result = await new LaneGitWorkspaceTools({ delegate: delegate(), resolveRoot }).execute(commandRequest); | ||
| assert.deepEqual(result, commandResult); | ||
| } | ||
| }); | ||
|
|
||
| test('the feature is advertised only when commands are', () => { | ||
| const withCommands = new LaneGitWorkspaceTools({ delegate: delegate(), resolveRoot: async () => undefined }); | ||
| assert.deepEqual(withCommands.capabilities.commandResultFeatures, ['lane_git']); | ||
| assert.equal(isValidBridgeWorkspaceToolCapabilities(withCommands.capabilities), true); | ||
| const readOnly = new LaneGitWorkspaceTools({ | ||
| delegate: { ...delegate(), capabilities: { protocolVersion: 1, operations: ['read_file'], workspaces: [{ id: 'primary' }] } }, | ||
| resolveRoot: async () => undefined, | ||
| }); | ||
| assert.equal(readOnly.capabilities.commandResultFeatures, undefined); | ||
| assert.equal( | ||
| isValidBridgeWorkspaceToolCapabilities({ ...withCommands.capabilities, commandResultFeatures: ['other'] }), | ||
| false, | ||
| ); | ||
| }); | ||
|
|
||
| test('the probe is skipped until lane_git is negotiated, then runs', async (t) => { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source'); | ||
| let negotiated = false; | ||
| let resolved = 0; | ||
| const tools = new LaneGitWorkspaceTools({ | ||
| delegate: delegate(), | ||
| isEnabled: () => negotiated, | ||
| resolveRoot: async () => { | ||
| resolved++; | ||
| return source; | ||
| }, | ||
| }); | ||
| const before = await tools.execute(commandRequest); | ||
| assert.equal('laneGit' in before, false); | ||
| assert.equal(resolved, 0); | ||
| negotiated = true; | ||
| const after = await tools.execute(commandRequest); | ||
| assert.equal(resolved, 1); | ||
| assert.deepEqual('laneGit' in after && after.laneGit, { branch: 'main', head: await sha(source) }); | ||
| }); | ||
|
|
||
| // Compatibility: negotiation with old and new Code API servers. | ||
|
|
||
| function quarantine() { | ||
| return { async assertAvailable() {}, async arm() {}, async clear() {}, async quarantine() {} }; | ||
| } | ||
|
|
||
| async function run(t: TestContext, supported: boolean) { | ||
| const root = await scratch(t); | ||
| const source = await repo(root, 'source'); | ||
| const tools = new LaneGitWorkspaceTools({ delegate: delegate(), resolveRoot: async () => source }); | ||
| const registrations: Array<{ commandResultFeatures?: string[] }> = []; | ||
| const settlements: Array<{ result?: Record<string, unknown> }> = []; | ||
| const worker = new BridgeWorker({ | ||
| codeApiUrl: 'https://code.example/v1', | ||
| token: 'worker-secret', | ||
| workerId: 'vm-1', | ||
| incarnationId, | ||
| sandboxEndpoint: 'http://127.0.0.1:2000/api/v2', | ||
| capabilities: { statefulWorkspace: false, sandboxProfile: 'anthropic-srt', runtimes: [], workspaceTools: tools.capabilities }, | ||
| workspaceTools: tools, | ||
| workspaceMutationQuarantine: quarantine(), | ||
| fetchImpl: async (input, init) => { | ||
| const url = String(input); | ||
| const body = init?.body ? JSON.parse(String(init.body)) : {}; | ||
| if (url.endsWith('/bridge/workers/register')) { | ||
| registrations.push(body.capabilities.workspaceTools ?? {}); | ||
| return Response.json({ | ||
| protocolVersion: 1, | ||
| workerId: 'vm-1', | ||
| incarnationId, | ||
| registeredAt: new Date().toISOString(), | ||
| leaseTtlMs: 60_000, | ||
| supportedWorkspaceToolOperations: ['read_file', 'execute_command'], | ||
| ...(supported ? { supportedWorkspaceCommandResultFeatures: ['lane_git'] } : {}), | ||
| }); | ||
| } | ||
| if (url.endsWith('/settle')) settlements.push(body); | ||
| return Response.json({ protocolVersion: 1, accepted: true }); | ||
| }, | ||
| }); | ||
| await worker.register(); | ||
| await worker.executeAndSettle({ | ||
| protocolVersion: 1, | ||
| assignmentId: 'assignment-1', | ||
| workerId: 'vm-1', | ||
| incarnationId, | ||
| generation: 1, | ||
| leaseToken: 'lease-token-that-is-long-enough-for-testing', | ||
| expiresAt: new Date(Date.now() + 5_000).toISOString(), | ||
| executionKind: 'workspace_tool', | ||
| workspaceId: 'primary', | ||
| request: commandRequest, | ||
| }); | ||
| return { registrations, settlements }; | ||
| } | ||
|
|
||
| test('a Code API without the feature still pairs, registers without it, and never receives laneGit', async (t) => { | ||
| const { registrations, settlements } = await run(t, false); | ||
| assert.ok(registrations.length > 0); | ||
| for (const registration of registrations) assert.equal('commandResultFeatures' in registration, false); | ||
| assert.equal(settlements.length, 1); | ||
| assert.equal(settlements[0]?.result?.exitCode, 0); | ||
| assert.equal('laneGit' in (settlements[0]?.result ?? {}), false); | ||
| }); | ||
|
|
||
| test('a Code API that acknowledges the feature registers it and receives laneGit', async (t) => { | ||
| const { registrations, settlements } = await run(t, true); | ||
| assert.deepEqual(registrations.at(-1)?.commandResultFeatures, ['lane_git']); | ||
| assert.deepEqual(Object.keys((settlements[0]?.result?.laneGit as object) ?? {}).sort(), ['branch', 'head']); | ||
| assert.equal((settlements[0]?.result?.laneGit as { branch: string }).branch, 'main'); | ||
| }); | ||
|
|
||
| test('a worker without the field still pairs and reports status', async () => { | ||
| const executor = { | ||
| capabilities: { protocolVersion: 1 as const, operations: ['read_file' as const], workspaces: [{ id: 'primary' }] }, | ||
| async execute(): Promise<never> { | ||
| throw new Error('not executed'); | ||
| }, | ||
| }; | ||
| const registrations: unknown[] = []; | ||
| const worker = new BridgeWorker({ | ||
| codeApiUrl: 'https://code.example/v1', | ||
| token: 'worker-secret', | ||
| workerId: 'vm-1', | ||
| incarnationId, | ||
| sandboxEndpoint: 'http://127.0.0.1:2000/api/v2', | ||
| capabilities: { statefulWorkspace: false, sandboxProfile: 'anthropic-srt', runtimes: [], workspaceTools: executor.capabilities }, | ||
| workspaceTools: executor, | ||
| fetchImpl: async (_input, init) => { | ||
| registrations.push(JSON.parse(String(init?.body)).capabilities.workspaceTools); | ||
| return Response.json({ | ||
| protocolVersion: 1, | ||
| workerId: 'vm-1', | ||
| incarnationId, | ||
| registeredAt: new Date().toISOString(), | ||
| leaseTtlMs: 60_000, | ||
| supportedWorkspaceToolOperations: ['read_file'], | ||
| }); | ||
| }, | ||
| }); | ||
| const registration = await worker.register(); | ||
| assert.equal(registration.workerId, 'vm-1'); | ||
| assert.ok(registrations.length > 0); | ||
| for (const advertised of registrations) { | ||
| assert.equal(JSON.stringify(advertised).includes('commandResultFeatures'), false); | ||
| assert.equal(JSON.stringify(advertised).includes('laneGit'), false); | ||
| } | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.