Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
63 changes: 63 additions & 0 deletions apps/cli/src/commands/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1542,6 +1542,69 @@ describe('session command helpers', () => {
}
});

it('continues local project resolution when machine Flock freshness sync fails', async () => {
const rootPath = mkdtempSync(path.join(os.tmpdir(), 'lody-session-git-project-'));
try {
execFileSync('git', ['init'], { cwd: rootPath, stdio: 'ignore' });
execFileSync(
'git',
[
'-c',
'user.name=Test',
'-c',
'user.email=test@example.com',
'commit',
'--allow-empty',
'-m',
'init',
],
{ cwd: rootPath, stdio: 'ignore' }
);
const syncFlockDocOrThrow = vi
.fn()
.mockRejectedValue(new Error('Streams sync failed: network_error'));
const manager = {
syncFlockDocOrThrow,
repo: {
getDocMeta: vi.fn(async () => ({
meta: {
localProjects: {
'local-project-1': {
id: 'local-project-1',
name: 'lody',
rootPath,
createdAtMs: 1,
},
},
},
})),
openFlockDoc: vi.fn(async () => ({
flock: {
scan: () => [],
},
})),
},
} as unknown as Parameters<typeof resolveLocalProjectRefOrThrow>[0];

await expect(
resolveLocalProjectRefOrThrow(
manager,
'workspace-1' as WorkspaceId,
'machine-id' as MachineId,
'lody'
)
).resolves.toEqual({
kind: 'local',
localProjectId: 'local-project-1',
});
expect(syncFlockDocOrThrow).toHaveBeenCalledWith(
getMachineFlockDocId('workspace-1' as WorkspaceId, 'machine-id' as MachineId),
expect.objectContaining({ reason: 'session.local-projects:machine-id' })
);
} finally {
rmSync(rootPath, { recursive: true, force: true });
}
});
it('does not synthesize a branch for non-git local projects', async () => {
const rootPath = mkdtempSync(path.join(os.tmpdir(), 'lody-session-non-git-'));
try {
Expand Down
93 changes: 78 additions & 15 deletions apps/cli/src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,31 @@ async function syncMachineFlockDocsForRead(
);
}

async function syncMachineFlockDocsForReadBestEffort(
manager: LoroDocumentManager,
workspaceId: WorkspaceId,
machineIds: readonly MachineId[],
reason: string
): Promise<Map<MachineId, string>> {
const logger = getLogger('session');
const syncErrors = new Map<MachineId, string>();
await Promise.all(
Array.from(new Set(machineIds)).map(async (machineId) => {
try {
await syncMachineFlockDocsForRead(manager, workspaceId, [machineId], reason);
} catch (error) {
const message = formatErrorMessage(error);
syncErrors.set(machineId, message);
logger.warn(
`Machine Flock freshness sync did not complete (${reason}:${machineId}); ` +
`continuing with the local replica: ${message}`
);
}
})
);
return syncErrors;
}

export async function resolveLocalProjectRefOrThrow(
manager: LoroDocumentManager,
workspaceId: WorkspaceId,
Expand All @@ -1013,12 +1038,22 @@ export async function resolveLocalProjectRefOrThrow(
requestedBranch?: string,
useWorktree?: boolean
): Promise<ProjectRef> {
await syncMachineFlockDocsForRead(manager, workspaceId, [machineId], 'session.local-projects');
const syncErrors = await syncMachineFlockDocsForReadBestEffort(
manager,
workspaceId,
[machineId],
'session.local-projects'
);
const syncError = syncErrors.get(machineId);
const syncErrorSuffix = syncError ? ` Flock freshness sync failed: ${syncError}` : '';
const withSyncError = (message: string): string => `${message}${syncErrorSuffix}`;
const localProjects = Object.values(
await readMachineLocalProjects(manager.repo, workspaceId, machineId)
);
if (localProjects.length === 0) {
throw new Error('No local project is registered on this machine for the target workspace.');
throw new Error(
withSyncError('No local project is registered on this machine for the target workspace.')
);
}

const normalizedSelector = normalizeCliValue(selector);
Expand All @@ -1030,9 +1065,11 @@ export async function resolveLocalProjectRefOrThrow(

if (matches.length === 0) {
throw new Error(
`Local project not found: ${normalizedSelector}. Candidates: ${localProjects
.map((project) => `${project.name} (${project.id})`)
.join(', ')}`
withSyncError(
`Local project not found: ${normalizedSelector}. Candidates: ${localProjects
.map((project) => `${project.name} (${project.id})`)
.join(', ')}`
)
);
}
if (matches.length > 1) {
Expand Down Expand Up @@ -2406,8 +2443,13 @@ async function listAgentConfigsForMachine(
manager: LoroDocumentManager,
workspaceId: WorkspaceId,
machineId: MachineId
): Promise<AgentConfigMeta[]> {
await syncMachineFlockDocsForRead(manager, workspaceId, [machineId], 'session.agent-configs');
): Promise<{ configs: AgentConfigMeta[]; syncError?: string }> {
const syncErrors = await syncMachineFlockDocsForReadBestEffort(
manager,
workspaceId,
[machineId],
'session.agent-configs'
);
const configs = await listMergedAgentConfigs(manager.repo, workspaceId, [machineId]);
configs.sort((left, right) => {
const nameCompare = left.name.localeCompare(right.name);
Expand All @@ -2416,7 +2458,8 @@ async function listAgentConfigsForMachine(
}
return left.id.localeCompare(right.id);
});
return configs;
const syncError = syncErrors.get(machineId);
return { configs, ...(syncError ? { syncError } : {}) };
}

export function selectDefaultAgentConfigForCreate(
Expand Down Expand Up @@ -2449,9 +2492,17 @@ async function resolveAgentConfigForCreate(args: {
selector?: string;
currentSession?: SessionMeta;
}): Promise<AgentConfigMeta> {
const configs = await listAgentConfigsForMachine(args.manager, args.workspaceId, args.machineId);
const { configs, syncError } = await listAgentConfigsForMachine(
args.manager,
args.workspaceId,
args.machineId
);
if (configs.length === 0) {
throw new Error(`No agent config exists on machine ${args.machineId}.`);
throw new Error(
`No agent config exists on machine ${args.machineId}.${
syncError ? ` Flock freshness sync failed: ${syncError}` : ''
}`
);
}
const selector =
normalizeCliValue(args.selector) ?? normalizeCliValue(process.env.LODY_AGENT_CONFIG_ID);
Expand Down Expand Up @@ -2683,12 +2734,22 @@ async function resolveLocalProjectRefOnMachineOrThrow(
requestedBranch?: string,
useWorktree?: boolean
): Promise<ProjectRef> {
await syncMachineFlockDocsForRead(manager, workspaceId, [machineId], 'session.local-projects');
const syncErrors = await syncMachineFlockDocsForReadBestEffort(
manager,
workspaceId,
[machineId],
'session.local-projects'
);
const syncError = syncErrors.get(machineId);
const syncErrorSuffix = syncError ? ` Flock freshness sync failed: ${syncError}` : '';
const withSyncError = (message: string): string => `${message}${syncErrorSuffix}`;
const localProjects = Object.values(
await readMachineLocalProjects(manager.repo, workspaceId, machineId)
);
if (localProjects.length === 0) {
throw new Error('No local project is registered on the target machine for this workspace.');
throw new Error(
withSyncError('No local project is registered on the target machine for this workspace.')
);
}
const authorizedLocalProjects = await filterAuthorizedLocalProjectsForCreate({
auth,
Expand All @@ -2707,9 +2768,11 @@ async function resolveLocalProjectRefOnMachineOrThrow(
const matches = selectLocalProjectsBySelector(authorizedLocalProjects, normalizedSelector);
if (matches.length === 0) {
throw new Error(
`Local project not found on ${machineId}: ${normalizedSelector}. Candidates: ${authorizedLocalProjects
.map((project) => `${project.name} (${project.id})`)
.join(', ')}`
withSyncError(
`Local project not found on ${machineId}: ${normalizedSelector}. Candidates: ${authorizedLocalProjects
.map((project) => `${project.name} (${project.id})`)
.join(', ')}`
)
);
}
if (matches.length > 1) {
Expand Down
Loading