diff --git a/src/main/services/team/TeamBootstrapStateReader.ts b/src/main/services/team/TeamBootstrapStateReader.ts index 1bce8d39..9ee6876d 100644 --- a/src/main/services/team/TeamBootstrapStateReader.ts +++ b/src/main/services/team/TeamBootstrapStateReader.ts @@ -363,7 +363,11 @@ async function readBootstrapLockMetadata(teamName: string): Promise { - return (await inspectBootstrapJournal(teamName)).warnings; + const inspection = await inspectBootstrapJournal(teamName); + const warnings = [inspection.issue, ...(inspection.warnings ?? [])].filter( + (item): item is string => typeof item === 'string' && item.trim().length > 0 + ); + return warnings.length > 0 ? warnings : undefined; } async function inspectBootstrapJournal(teamName: string): Promise { @@ -436,6 +440,13 @@ async function inspectBootstrapJournal(teamName: string): Promise Boolean(item)); + if (lines.length > 0 && messages.length === 0) { + return { + issue: + 'Persisted deterministic bootstrap journal is unreadable because bootstrap-journal.jsonl is invalid, truncated, or inaccessible.', + }; + } + return { warnings: messages.length > 0 diff --git a/test/main/services/team/TeamBootstrapStateReader.test.ts b/test/main/services/team/TeamBootstrapStateReader.test.ts index f783fa76..31e7f928 100644 --- a/test/main/services/team/TeamBootstrapStateReader.test.ts +++ b/test/main/services/team/TeamBootstrapStateReader.test.ts @@ -142,6 +142,41 @@ describe('TeamBootstrapStateReader', () => { nowSpy.mockRestore(); }); + it('surfaces unreadable bootstrap journal as a warning without breaking active recovery', async () => { + const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true as never); + + hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { + contents: JSON.stringify({ + version: 1, + runId: 'run-123', + teamName: 'demo', + ownerPid: 4242, + startedAt: 1700000000000, + updatedAt: 1700000000500, + phase: 'spawning_members', + members: [{ name: 'alice', status: 'pending' }], + }), + }); + hoisted.files.set('/mock/teams/demo/bootstrap-journal.jsonl', { + contents: '{invalid-json', + }); + + await expect(readBootstrapRuntimeState('demo')).resolves.toMatchObject({ + teamName: 'demo', + isAlive: false, + runId: 'run-123', + progress: { + state: 'assembling', + message: 'Spawning teammate runtimes (1)', + warnings: [ + 'Persisted deterministic bootstrap journal is unreadable because bootstrap-journal.jsonl is invalid, truncated, or inaccessible.', + ], + }, + }); + + killSpy.mockRestore(); + }); + it('ignores terminal bootstrap-state for runtime recovery projection', async () => { hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: JSON.stringify({