From 18fe31f1071c3e845b671e72610bb1d00b0bb80e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 11 Aug 2026 11:15:41 -0400 Subject: [PATCH] fix(skills): return retryable rename exhaustion --- .../skill-install-transaction-result.ts | 2 +- .../skills/skill-install-transaction.test.ts | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/skills/skill-install-transaction-result.ts b/src/main/skills/skill-install-transaction-result.ts index 8c84746e592..4f048e1ae06 100644 --- a/src/main/skills/skill-install-transaction-result.ts +++ b/src/main/skills/skill-install-transaction-result.ts @@ -40,7 +40,7 @@ export function skillInstallFailureResult( error: unknown ): SkillInstallResult | null { const failure = skillInstallFailureFromError(error) - if (!failure || (failure.code !== 'skill-install-busy' && failure.category !== 'cancelled')) { + if (!failure || !['filesystem', 'recovery', 'cancelled'].includes(failure.category)) { return null } return { diff --git a/src/main/skills/skill-install-transaction.test.ts b/src/main/skills/skill-install-transaction.test.ts index d4326d64c00..97bad22972f 100644 --- a/src/main/skills/skill-install-transaction.test.ts +++ b/src/main/skills/skill-install-transaction.test.ts @@ -186,6 +186,35 @@ describe('skill install transaction', () => { ) }) + it('returns a retryable result after rename exhaustion preserves the old version', async () => { + const root = await temporaryDirectory() + const first = await packageVersion(root, 'version_1', '# First') + const second = await packageVersion(root, 'version_2', '# Second') + await installLocalSkillPackage(installInput(root, first)) + const busyFilesystem = { + ...nativeSkillInstallFilesystem, + rename: async (source: string, target: string): Promise => { + if (target.includes('.orca-backup-')) { + throw Object.assign(new Error('locked by scanner'), { code: 'EBUSY' }) + } + await nativeSkillInstallFilesystem.rename(source, target) + } + } + + const result = await installLocalSkillPackage( + installInput(root, second, { filesystem: busyFilesystem }) + ) + + expect(result).toMatchObject({ + status: 'failed', + errorCategory: 'skill-install-filesystem-failed', + failure: { category: 'filesystem', retryable: true } + }) + expect(await readFile(join(root, 'skills', 'test-skill', 'SKILL.md'), 'utf8')).toContain( + '# First' + ) + }) + it('returns a retryable failure when another process owns the destination lock', async () => { const root = await temporaryDirectory() const archive = await packageVersion(root, 'version_1', '# First')