diff --git a/packages/core/src/agents/acknowledgedAgents.test.ts b/packages/core/src/agents/acknowledgedAgents.test.ts index f6e45360db6..2fe59253d43 100644 --- a/packages/core/src/agents/acknowledgedAgents.test.ts +++ b/packages/core/src/agents/acknowledgedAgents.test.ts @@ -94,4 +94,34 @@ describe('AcknowledgedAgentsService', () => { false, ); }); + + it.each([ + 'null', + '42', + '"str"', + '[]', + '{"/project": "str"}', + '{"/project": 42}', + '{"/project": null}', + '{"/project": []}', + ])( + 'should fall back to empty for valid JSON with the wrong shape (%s, #29207)', + async (content) => { + const ackPath = Storage.getAcknowledgedAgentsPath(); + await fs.mkdir(path.dirname(ackPath), { recursive: true }); + await fs.writeFile(ackPath, content, 'utf-8'); + + const service = new AcknowledgedAgentsService(); + + await expect( + service.isAcknowledged('/project', 'Agent', 'hash'), + ).resolves.toBe(false); + await expect( + service.acknowledge('/project', 'Agent', 'hash'), + ).resolves.toBeUndefined(); + await expect( + service.isAcknowledged('/project', 'Agent', 'hash'), + ).resolves.toBe(true); + }, + ); }); diff --git a/packages/core/src/agents/acknowledgedAgents.ts b/packages/core/src/agents/acknowledgedAgents.ts index 98c90afb96c..6cb4af3e21d 100644 --- a/packages/core/src/agents/acknowledgedAgents.ts +++ b/packages/core/src/agents/acknowledgedAgents.ts @@ -17,6 +17,20 @@ export interface AcknowledgedAgentsMap { }; } +function isAcknowledgedAgentsMap( + value: unknown, +): value is AcknowledgedAgentsMap { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + // Each project entry must itself be a map object: a truthy primitive + // (e.g. {"/project": "str"}) would otherwise pass this guard and then + // crash acknowledge() with "Cannot create property on string". + return Object.values(value).every( + (entry) => !!entry && typeof entry === 'object' && !Array.isArray(entry), + ); +} + export class AcknowledgedAgentsService { private acknowledgedAgents: AcknowledgedAgentsMap = {}; private loaded = false; @@ -27,8 +41,18 @@ export class AcknowledgedAgentsService { const filePath = Storage.getAcknowledgedAgentsPath(); try { const content = await fs.readFile(filePath, 'utf-8'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this.acknowledgedAgents = JSON.parse(content); + const parsed: unknown = JSON.parse(content); + // A previous interrupted save (full disk, sync conflict, hand edit) + // can leave valid JSON with the wrong shape (null, array, scalar). + // Fall back to empty rather than crashing callers (#29207). + if (isAcknowledgedAgentsMap(parsed)) { + this.acknowledgedAgents = parsed; + } else { + debugLogger.error( + 'Failed to load acknowledged agents: unexpected file shape, falling back to empty.', + ); + this.acknowledgedAgents = {}; + } } catch (error: unknown) { if (!isNodeError(error) || error.code !== 'ENOENT') { debugLogger.error(