From 3778c055bc125ccf8de68076f47f6da2edaefd5a Mon Sep 17 00:00:00 2001 From: UniversePeak <113168673+UniversePeak@users.noreply.github.com> Date: Sat, 5 Sep 2026 11:40:55 +0800 Subject: [PATCH 1/2] fix(cli): allow local session creation on stale Flock sync Model: gpt-5.6-luna --- apps/cli/src/commands/session.test.ts | 6 ++++-- apps/cli/src/commands/session.ts | 25 +++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/cli/src/commands/session.test.ts b/apps/cli/src/commands/session.test.ts index 5d97136ca..73c97c048 100644 --- a/apps/cli/src/commands/session.test.ts +++ b/apps/cli/src/commands/session.test.ts @@ -1020,7 +1020,7 @@ describe('session command helpers', () => { ).toEqual([authorizedProject]); }); - it('marks local project refs for worktree session creation', async () => { + 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' }); @@ -1038,7 +1038,9 @@ describe('session command helpers', () => { ], { cwd: rootPath, stdio: 'ignore' } ); - const syncFlockDocOrThrow = vi.fn(async () => undefined); + const syncFlockDocOrThrow = vi + .fn() + .mockRejectedValueOnce(new Error('Streams sync failed: network_error')); const manager = { syncFlockDocOrThrow, repo: { diff --git a/apps/cli/src/commands/session.ts b/apps/cli/src/commands/session.ts index 198bf0d26..87fe4785e 100644 --- a/apps/cli/src/commands/session.ts +++ b/apps/cli/src/commands/session.ts @@ -984,6 +984,27 @@ async function syncMachineFlockDocsForRead( ); } +async function syncMachineFlockDocsForReadBestEffort( + manager: LoroDocumentManager, + workspaceId: WorkspaceId, + machineIds: readonly MachineId[], + reason: string +): Promise { + const logger = getLogger('session'); + await Promise.all( + Array.from(new Set(machineIds)).map(async (machineId) => { + try { + await syncMachineFlockDocsForRead(manager, workspaceId, [machineId], reason); + } catch (error) { + logger.warn( + `Machine Flock freshness sync did not complete (${reason}:${machineId}); ` + + `continuing with the local replica: ${formatErrorMessage(error)}` + ); + } + }) + ); +} + export async function resolveLocalProjectRefOrThrow( manager: LoroDocumentManager, workspaceId: WorkspaceId, @@ -992,7 +1013,7 @@ export async function resolveLocalProjectRefOrThrow( requestedBranch?: string, useWorktree?: boolean ): Promise { - await syncMachineFlockDocsForRead(manager, workspaceId, [machineId], 'session.local-projects'); + await syncMachineFlockDocsForReadBestEffort(manager, workspaceId, [machineId], 'session.local-projects'); const localProjects = Object.values( await readMachineLocalProjects(manager.repo, workspaceId, machineId) ); @@ -2457,7 +2478,7 @@ async function resolveLocalProjectRefOnMachineOrThrow( requestedBranch?: string, useWorktree?: boolean ): Promise { - await syncMachineFlockDocsForRead(manager, workspaceId, [machineId], 'session.local-projects'); + await syncMachineFlockDocsForReadBestEffort(manager, workspaceId, [machineId], 'session.local-projects'); const localProjects = Object.values( await readMachineLocalProjects(manager.repo, workspaceId, machineId) ); From ba5253a69cd8f801ee58e0721403bbf962276786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=87=E5=AE=99=E7=9A=84=E5=B1=B1=E5=B3=B0?= <113168673+UniversePeak@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:10:52 +0800 Subject: [PATCH 2/2] fix(cli): tolerate stale Flock sync during session create Model: gpt-5.6-luna --- apps/cli/src/commands/session.test.ts | 69 ++++++++++++++++++++++-- apps/cli/src/commands/session.ts | 76 +++++++++++++++++++++------ 2 files changed, 124 insertions(+), 21 deletions(-) diff --git a/apps/cli/src/commands/session.test.ts b/apps/cli/src/commands/session.test.ts index 73c97c048..f6848c36f 100644 --- a/apps/cli/src/commands/session.test.ts +++ b/apps/cli/src/commands/session.test.ts @@ -1020,7 +1020,7 @@ describe('session command helpers', () => { ).toEqual([authorizedProject]); }); - it('continues local project resolution when machine Flock freshness sync fails', async () => { + it('marks local project refs for worktree session creation', async () => { const rootPath = mkdtempSync(path.join(os.tmpdir(), 'lody-session-git-project-')); try { execFileSync('git', ['init'], { cwd: rootPath, stdio: 'ignore' }); @@ -1038,9 +1038,7 @@ describe('session command helpers', () => { ], { cwd: rootPath, stdio: 'ignore' } ); - const syncFlockDocOrThrow = vi - .fn() - .mockRejectedValueOnce(new Error('Streams sync failed: network_error')); + const syncFlockDocOrThrow = vi.fn(async () => undefined); const manager = { syncFlockDocOrThrow, repo: { @@ -1099,6 +1097,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[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 { diff --git a/apps/cli/src/commands/session.ts b/apps/cli/src/commands/session.ts index 87fe4785e..11dc6e70d 100644 --- a/apps/cli/src/commands/session.ts +++ b/apps/cli/src/commands/session.ts @@ -989,20 +989,24 @@ async function syncMachineFlockDocsForReadBestEffort( workspaceId: WorkspaceId, machineIds: readonly MachineId[], reason: string -): Promise { +): Promise> { const logger = getLogger('session'); + const syncErrors = new Map(); 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: ${formatErrorMessage(error)}` + `continuing with the local replica: ${message}` ); } }) ); + return syncErrors; } export async function resolveLocalProjectRefOrThrow( @@ -1013,12 +1017,22 @@ export async function resolveLocalProjectRefOrThrow( requestedBranch?: string, useWorktree?: boolean ): Promise { - await syncMachineFlockDocsForReadBestEffort(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); @@ -1030,9 +1044,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) { @@ -2201,8 +2217,13 @@ async function listAgentConfigsForMachine( manager: LoroDocumentManager, workspaceId: WorkspaceId, machineId: MachineId -): Promise { - 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); @@ -2211,7 +2232,8 @@ async function listAgentConfigsForMachine( } return left.id.localeCompare(right.id); }); - return configs; + const syncError = syncErrors.get(machineId); + return { configs, ...(syncError ? { syncError } : {}) }; } export function selectDefaultAgentConfigForCreate( @@ -2244,9 +2266,17 @@ async function resolveAgentConfigForCreate(args: { selector?: string; currentSession?: SessionMeta; }): Promise { - 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); @@ -2478,12 +2508,22 @@ async function resolveLocalProjectRefOnMachineOrThrow( requestedBranch?: string, useWorktree?: boolean ): Promise { - await syncMachineFlockDocsForReadBestEffort(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, @@ -2502,9 +2542,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) {