From 759e536e5fb39eefd8c382928a366dfeafc3688f Mon Sep 17 00:00:00 2001 From: Sunhy <58774225+Shy7777@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:00:33 +0800 Subject: [PATCH] Preserve concurrent edits during remote Hermes reinstall (#20631) Hermes remote install only rewrites config.yaml when its content changed, so a stale snapshot can no longer overwrite a concurrent edit (matches local install). --- .../hermes-config-remote-adapter.test.ts | 23 +++++++++++++++++++ src/main/hermes/hook-service.ts | 4 +++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/hermes/hermes-config-remote-adapter.test.ts b/src/main/hermes/hermes-config-remote-adapter.test.ts index 2c860b85798..d6a57c5258a 100644 --- a/src/main/hermes/hermes-config-remote-adapter.test.ts +++ b/src/main/hermes/hermes-config-remote-adapter.test.ts @@ -46,6 +46,29 @@ describe('Hermes remote installer through a local filesystem adapter (no SSH)', expect(readFileSync(`${configPath}.bak`, 'utf8')).toBe('recovery point') }) + it('keeps edits made during an already-configured remote install', async () => { + writeFileSync(configPath, 'model: initial\n') + // Why: derive "already configured" from a real install so the fixture tracks managed-config changes. + await service.installRemote(createManagedHookLocalFilesystem(), directory) + rmSync(join(directory, '.hermes', 'plugins'), { recursive: true }) + const updated = readFileSync(configPath, 'utf8').replace( + 'model: initial', + 'model: operator-edited' + ) + expect(updated).toContain('model: operator-edited') + const filesystem = createManagedHookLocalFilesystem() + const rename = filesystem.ext_openssh_rename.bind(filesystem) + filesystem.ext_openssh_rename = (source, destination, callback) => { + if (destination === join(directory, '.hermes', 'plugins', 'orca-status', 'plugin.yaml')) { + writeFileSync(configPath, updated) + } + rename(source, destination, callback) + } + + expect((await service.installRemote(filesystem, directory)).state).toBe('installed') + expect(readFileSync(configPath, 'utf8')).toBe(updated) + }) + it.each(['\n', '\r\n'])( 'preserves comments and parsed values during real install (%j)', async (eol) => { diff --git a/src/main/hermes/hook-service.ts b/src/main/hermes/hook-service.ts index 6b6d6ec8873..d6bd992e23e 100644 --- a/src/main/hermes/hook-service.ts +++ b/src/main/hermes/hook-service.ts @@ -131,7 +131,9 @@ export class HermesHookService { } await writeTextFileRemoteAtomic(sftp, `${remotePluginDir}/plugin.yaml`, getPluginManifest()) await writeTextFileRemoteAtomic(sftp, `${remotePluginDir}/__init__.py`, getPluginInitSource()) - await writeTextFileRemoteAtomic(sftp, remoteConfigPath, next.content) + if (next.content !== existing) { + await writeTextFileRemoteAtomic(sftp, remoteConfigPath, next.content) + } return { agent: 'hermes', state: 'installed',