From f5d56fe91e1f0a4db39676c976cf8560fb5a2987 Mon Sep 17 00:00:00 2001 From: UniversePeak <113168673+UniversePeak@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:38:56 +0800 Subject: [PATCH 1/2] fix(cli): support custom ACP history synchronization Model: gpt-5.6-luna --- .../src/lib/history-session-catalog-client.ts | 19 ++++++-- .../lib/local-project-history-sync-service.ts | 48 +++++++++++++++---- .../history-session-catalog-client.test.ts | 17 +++++++ .../shared/src/node/local-project-control.ts | 2 +- .../tests/local-project-control.test.ts | 26 ++++++++++ 5 files changed, 96 insertions(+), 16 deletions(-) diff --git a/apps/cli/src/lib/history-session-catalog-client.ts b/apps/cli/src/lib/history-session-catalog-client.ts index 2ae409bbe..dfb7f38f9 100644 --- a/apps/cli/src/lib/history-session-catalog-client.ts +++ b/apps/cli/src/lib/history-session-catalog-client.ts @@ -24,6 +24,7 @@ import { type ACPSessionId, getLocalProjectHistoryProviderKey, type LocalProjectHistoryProvider, + type CustomAcpLaunchSpec, } from '@lody/shared'; import { LODY_EXTENSION_METHODS } from 'acp-extension-core'; @@ -126,11 +127,19 @@ type ResolvedHistoryACPProcessLaunch = Omit & { env: NodeJS.ProcessEnv; }; +export type HistoryLaunchProvider = LocalProjectHistoryProvider & { + customAcp?: CustomAcpLaunchSpec; +}; + export async function resolveHistoryACPProcessLaunch(args: { - provider: LocalProjectHistoryProvider; + provider: HistoryLaunchProvider; env?: NodeJS.ProcessEnv; }): Promise { - const launch = await resolveACPProcessLaunchAsync(args.provider); + const launch = await resolveACPProcessLaunchAsync({ + cliType: args.provider.cliType, + agentType: args.provider.agentType, + customAcp: args.provider.customAcp, + }); return { ...launch, env: mergeACPProcessEnv(launch, args.env ?? process.env), @@ -138,7 +147,7 @@ export async function resolveHistoryACPProcessLaunch(args: { } async function createHistoryAcpConnection(args: { - provider: LocalProjectHistoryProvider; + provider: HistoryLaunchProvider; workdir: string; logger: Logger; }): Promise { @@ -327,7 +336,7 @@ export function dedupeHistorySessionsById(sessions: SessionInfo[]): SessionInfo[ } export async function listHistorySessionsForLocalProject(args: { - provider: LocalProjectHistoryProvider; + provider: HistoryLaunchProvider; rootPath: string; logger: Logger; requiredSessionIds?: readonly string[]; @@ -376,7 +385,7 @@ export async function listHistorySessionsForLocalProject(args: { } export async function loadHistorySessionReplay(args: { - provider: LocalProjectHistoryProvider; + provider: HistoryLaunchProvider; rootPath: string; acpSessionId: ACPSessionId; logger: Logger; diff --git a/apps/cli/src/lib/local-project-history-sync-service.ts b/apps/cli/src/lib/local-project-history-sync-service.ts index cb75527fc..c680bda9e 100644 --- a/apps/cli/src/lib/local-project-history-sync-service.ts +++ b/apps/cli/src/lib/local-project-history-sync-service.ts @@ -35,8 +35,10 @@ import { import type { LoroDocumentManager, SessionDocument } from '@/lib/loro/doc'; import { readMachineLocalProjects, upsertMachineLocalProject } from '@/lib/local-project-meta'; +import { listMergedAgentConfigs } from '@/lib/agent-config-machine-flock'; import { listHistorySessionsForLocalProject, + type HistoryLaunchProvider, loadHistorySessionReplay, MAX_LOCAL_PROJECT_HISTORY_CATALOG_SESSIONS, } from './history-session-catalog-client'; @@ -546,6 +548,7 @@ function buildExternalHistoryMeta(args: { export class LocalProjectHistorySyncService { private readonly provider: LocalProjectHistoryProvider; private readonly providerKey: string; + private launchProvider: HistoryLaunchProvider; constructor( private readonly manager: LoroDocumentManager, @@ -559,6 +562,25 @@ export class LocalProjectHistorySyncService { ) { this.provider = provider; this.providerKey = getLocalProjectHistoryProviderKey(provider); + this.launchProvider = provider; + } + + private async resolveLaunchProvider(): Promise { + if (this.provider.cliType !== 'custom') { + return this.provider; + } + const configs = await listMergedAgentConfigs(this.manager.repo, this.context.workspaceId, [ + this.context.machineId, + ]); + const config = configs.find( + (candidate) => + candidate.machineId === this.context.machineId && + candidate.cliType === this.provider.cliType && + candidate.agentType === this.provider.agentType + ); + const resolved = { ...this.provider, customAcp: config?.customAcp }; + this.launchProvider = resolved; + return resolved; } async syncLocalProject(args: { @@ -585,7 +607,8 @@ export class LocalProjectHistorySyncService { localProjectId: LocalProjectId; rootPath: string; }): Promise { - const snapshot = await this.listCatalogSnapshot(args); + this.launchProvider = await this.resolveLaunchProvider(); + const snapshot = await this.listCatalogSnapshot(args, this.launchProvider); return await this.writeCatalogResult({ localProjectId: args.localProjectId, sessions: snapshot.sessions, @@ -644,6 +667,7 @@ export class LocalProjectHistorySyncService { const summary = emptySummary(); const selectedIds = [...new Set(args.acpSessionIds)]; summary.listed = selectedIds.length; + this.launchProvider = await this.resolveLaunchProvider(); const snapshot = await this.listCatalogSnapshot({ ...args, requiredSessionIds: selectedIds, @@ -672,7 +696,7 @@ export class LocalProjectHistorySyncService { snapshot.existingByImportKey.get(importKey); if (!existing) { const replayNotifications = await loadHistorySessionReplay({ - provider: this.provider, + provider: this.launchProvider, rootPath: args.rootPath, acpSessionId, logger: this.logger, @@ -730,6 +754,7 @@ export class LocalProjectHistorySyncService { sessionId: SessionId; acpSessionId: string; }): Promise { + this.launchProvider = await this.resolveLaunchProvider(); const snapshot = await this.listCatalogSnapshot({ ...args, requiredSessionIds: [args.acpSessionId], @@ -812,7 +837,7 @@ export class LocalProjectHistorySyncService { const acpSessionId = args.acpSessionId as unknown as ACPSessionId; const replayNotifications = await loadHistorySessionReplay({ - provider: this.provider, + provider: this.launchProvider, rootPath: args.rootPath, acpSessionId, logger: this.logger, @@ -906,13 +931,16 @@ export class LocalProjectHistorySyncService { }); } - private async listCatalogSnapshot(args: { - localProjectId: LocalProjectId; - rootPath: string; - requiredSessionIds?: readonly string[]; - }): Promise { + private async listCatalogSnapshot( + args: { + localProjectId: LocalProjectId; + rootPath: string; + requiredSessionIds?: readonly string[]; + }, + provider: HistoryLaunchProvider = this.launchProvider + ): Promise { const catalog = await listHistorySessionsForLocalProject({ - provider: this.provider, + provider, rootPath: args.rootPath, logger: this.logger, requiredSessionIds: args.requiredSessionIds, @@ -1124,7 +1152,7 @@ export class LocalProjectHistorySyncService { } const replayNotifications = await loadHistorySessionReplay({ - provider: this.provider, + provider: this.launchProvider, rootPath: args.rootPath, acpSessionId: args.acpSessionId, logger: this.logger, diff --git a/apps/cli/tests/history-session-catalog-client.test.ts b/apps/cli/tests/history-session-catalog-client.test.ts index a03c29990..2af5643e6 100644 --- a/apps/cli/tests/history-session-catalog-client.test.ts +++ b/apps/cli/tests/history-session-catalog-client.test.ts @@ -217,6 +217,23 @@ describe('resolveHistoryACPProcessLaunch', () => { expect(historyLaunch.env.PATH).toBe('/usr/bin'); }); + it('resolves a custom ACP launch from the provider launch spec', async () => { + const historyLaunch = await resolveHistoryACPProcessLaunch({ + provider: { + cliType: 'custom', + agentType: 'my-agent', + customAcp: { + command: process.execPath, + args: ['custom-acp-server.mjs', '--stdio'], + }, + }, + env: { PATH: '/usr/bin' }, + }); + + expect(historyLaunch.command).toBe(process.execPath); + expect(historyLaunch.args).toEqual(['custom-acp-server.mjs', '--stdio']); + expect(historyLaunch.env.PATH).toBe('/usr/bin'); + }); it('uses the same registry Interactive Claude npx launch as normal sessions', async () => { const provider = { cliType: 'registry', agentType: 'claude-p' } as const; const sessionLaunch = resolveACPProcessLaunch(provider); diff --git a/packages/shared/src/node/local-project-control.ts b/packages/shared/src/node/local-project-control.ts index f103379ef..9eeda6deb 100644 --- a/packages/shared/src/node/local-project-control.ts +++ b/packages/shared/src/node/local-project-control.ts @@ -39,7 +39,7 @@ function isStringArray(value: unknown): value is string[] { function isLocalProjectHistoryProvider(value: unknown): boolean { return ( isObjectRecord(value) && - (value.cliType === 'builtin' || value.cliType === 'registry') && + (value.cliType === 'builtin' || value.cliType === 'registry' || value.cliType === 'custom') && typeof value.agentType === 'string' && value.agentType.trim().length > 0 ); diff --git a/packages/shared/tests/local-project-control.test.ts b/packages/shared/tests/local-project-control.test.ts index c58614b5d..4cf6c44a0 100644 --- a/packages/shared/tests/local-project-control.test.ts +++ b/packages/shared/tests/local-project-control.test.ts @@ -10,6 +10,10 @@ import { const codexProvider = { cliType: 'builtin', agentType: 'codex' } as const; const claudeProvider = { cliType: 'builtin', agentType: 'claude' } as const; +const customProvider = { + cliType: 'custom', + agentType: 'custom-agent', +} as const; describe('local project control request schema', () => { it('parses add request', () => { @@ -198,6 +202,28 @@ describe('local project control request schema', () => { }); }); + it('parses custom provider history sync requests', () => { + const parsed = safeParseLocalProjectControlRequest( + JSON.stringify({ + type: 'local-project/sync-history', + machineId: 'machine-1', + workspaceId: 'workspace-1', + localProjectId: 'project-1', + provider: customProvider, + }) + ); + + expect(parsed.success).toBe(true); + if (!parsed.success) { + return; + } + if (parsed.data.type !== 'local-project/sync-history') { + throw new Error(`Unexpected request type: ${parsed.data.type}`); + } + + expect(parsed.data.provider).toEqual(customProvider); + }); + it('parses Codex provider history import request with selected sessions', () => { const parsed = safeParseLocalProjectControlRequest( JSON.stringify({ From 87f89b469cdb81857b2b119fe07f711aca1a6f7d Mon Sep 17 00:00:00 2001 From: UniversePeak <113168673+UniversePeak@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:17:35 +0800 Subject: [PATCH 2/2] fix(cli): propagate custom ACP history environment Model: gpt-5.6-luna --- .../src/lib/history-session-catalog-client.ts | 9 ++++++- .../lib/local-project-history-sync-service.ts | 6 ++++- .../history-session-catalog-client.test.ts | 24 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/apps/cli/src/lib/history-session-catalog-client.ts b/apps/cli/src/lib/history-session-catalog-client.ts index dfb7f38f9..476f6a466 100644 --- a/apps/cli/src/lib/history-session-catalog-client.ts +++ b/apps/cli/src/lib/history-session-catalog-client.ts @@ -25,6 +25,7 @@ import { getLocalProjectHistoryProviderKey, type LocalProjectHistoryProvider, type CustomAcpLaunchSpec, + type BuiltinRuntimeOverrides, } from '@lody/shared'; import { LODY_EXTENSION_METHODS } from 'acp-extension-core'; @@ -129,6 +130,8 @@ type ResolvedHistoryACPProcessLaunch = Omit & { export type HistoryLaunchProvider = LocalProjectHistoryProvider & { customAcp?: CustomAcpLaunchSpec; + runtimeOverrides?: BuiltinRuntimeOverrides; + env?: NodeJS.ProcessEnv; }; export async function resolveHistoryACPProcessLaunch(args: { @@ -139,10 +142,14 @@ export async function resolveHistoryACPProcessLaunch(args: { cliType: args.provider.cliType, agentType: args.provider.agentType, customAcp: args.provider.customAcp, + runtimeOverrides: args.provider.runtimeOverrides, }); return { ...launch, - env: mergeACPProcessEnv(launch, args.env ?? process.env), + env: mergeACPProcessEnv(launch, { + ...(args.env ?? process.env), + ...(args.provider.env ?? {}), + }), }; } diff --git a/apps/cli/src/lib/local-project-history-sync-service.ts b/apps/cli/src/lib/local-project-history-sync-service.ts index c680bda9e..3e6b573fd 100644 --- a/apps/cli/src/lib/local-project-history-sync-service.ts +++ b/apps/cli/src/lib/local-project-history-sync-service.ts @@ -578,7 +578,11 @@ export class LocalProjectHistorySyncService { candidate.cliType === this.provider.cliType && candidate.agentType === this.provider.agentType ); - const resolved = { ...this.provider, customAcp: config?.customAcp }; + const resolved = { + ...this.provider, + customAcp: config?.customAcp, + env: config?.env, + }; this.launchProvider = resolved; return resolved; } diff --git a/apps/cli/tests/history-session-catalog-client.test.ts b/apps/cli/tests/history-session-catalog-client.test.ts index 2af5643e6..f7d4c4d14 100644 --- a/apps/cli/tests/history-session-catalog-client.test.ts +++ b/apps/cli/tests/history-session-catalog-client.test.ts @@ -234,6 +234,30 @@ describe('resolveHistoryACPProcessLaunch', () => { expect(historyLaunch.args).toEqual(['custom-acp-server.mjs', '--stdio']); expect(historyLaunch.env.PATH).toBe('/usr/bin'); }); + + it('merges the authoritative custom ACP environment before spawning history', async () => { + const historyLaunch = await resolveHistoryACPProcessLaunch({ + provider: { + cliType: 'custom', + agentType: 'my-agent', + customAcp: { command: process.execPath }, + env: { + ACP_API_KEY: 'config-secret', + ACP_BASE_URL: 'https://config.example.test', + PATH: '/config/bin', + }, + }, + env: { + ACP_API_KEY: 'caller-secret', + PATH: '/caller/bin', + }, + }); + + expect(historyLaunch.env.ACP_API_KEY).toBe('config-secret'); + expect(historyLaunch.env.ACP_BASE_URL).toBe('https://config.example.test'); + expect(historyLaunch.env.PATH).toBe('/config/bin'); + }); + it('uses the same registry Interactive Claude npx launch as normal sessions', async () => { const provider = { cliType: 'registry', agentType: 'claude-p' } as const; const sessionLaunch = resolveACPProcessLaunch(provider);