diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 1960e7a8..0c5685b2 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -35,6 +35,7 @@ import { resolveLanguageName } from '@shared/utils/agentLanguage'; import { parseCliArgs } from '@shared/utils/cliArgsParser'; import { isInboxNoiseMessage, + isMeaningfulBootstrapCheckInMessage, type ParsedPermissionRequest, parsePermissionRequest, } from '@shared/utils/inboxNoise'; @@ -2593,7 +2594,10 @@ export class TeamProvisioningService { // runtime produced a real post-spawn message, unlike writes to inboxes/.json // which may simply be user/lead messages addressed TO the teammate. const sameTeamBlocks = blocks.filter((block) => !parseCrossTeamPrefix(block.content)); - for (const block of sameTeamBlocks) { + const meaningfulSameTeamBlocks = sameTeamBlocks.filter((block) => + isMeaningfulBootstrapCheckInMessage(block.content) + ); + for (const block of meaningfulSameTeamBlocks) { this.setMemberSpawnStatus(run, block.teammateId, 'online', undefined, 'heartbeat'); } for (const block of sameTeamBlocks) { @@ -6491,7 +6495,12 @@ export class TeamProvisioningService { matchedRuntimeNames.some((runtimeName) => liveAgentNames.has(runtimeName)); const heartbeatMessage = leadInboxMessages.find((message) => { if (typeof message.from !== 'string' || message.from.trim() !== expected) return false; - if (typeof message.text !== 'string' || message.text.trim().length === 0) return false; + if ( + typeof message.text !== 'string' || + !isMeaningfulBootstrapCheckInMessage(message.text) + ) { + return false; + } const firstAcceptedAt = current.firstSpawnAcceptedAt ? Date.parse(current.firstSpawnAcceptedAt) : NaN; diff --git a/src/renderer/components/team/TeamProvisioningBanner.tsx b/src/renderer/components/team/TeamProvisioningBanner.tsx index 087323be..661df1cd 100644 --- a/src/renderer/components/team/TeamProvisioningBanner.tsx +++ b/src/renderer/components/team/TeamProvisioningBanner.tsx @@ -180,7 +180,7 @@ export const TeamProvisioningBanner = ({ 0 ? readyDetailMessage : null} messageSeverity={readyDetailSeverity} currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1} startedAt={progress.startedAt} diff --git a/src/shared/utils/inboxNoise.ts b/src/shared/utils/inboxNoise.ts index 0886e309..36c2cdaa 100644 --- a/src/shared/utils/inboxNoise.ts +++ b/src/shared/utils/inboxNoise.ts @@ -37,6 +37,16 @@ export function isInboxNoiseMessage(text: string): boolean { return !!type && INBOX_NOISE_SET.has(type); } +/** + * Returns true when a teammate message is substantive enough to count as a real + * bootstrap/check-in signal. Internal coordination noise like idle/shutdown + * JSON should keep the runtime marked alive, but must not be treated as a + * teammate "checked in" signal in launch UX. + */ +export function isMeaningfulBootstrapCheckInMessage(text: string): boolean { + return text.trim().length > 0 && !isInboxNoiseMessage(text); +} + // --------------------------------------------------------------------------- // Teammate permission request parsing // --------------------------------------------------------------------------- diff --git a/test/shared/utils/inboxNoise.test.ts b/test/shared/utils/inboxNoise.test.ts index cda94fff..033fd803 100644 --- a/test/shared/utils/inboxNoise.test.ts +++ b/test/shared/utils/inboxNoise.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { isInboxNoiseMessage, + isMeaningfulBootstrapCheckInMessage, isOnlyTeammateMessageBlocks, isThoughtProtocolNoise, stripTeammateMessageBlocks, @@ -124,3 +125,21 @@ describe('isInboxNoiseMessage', () => { expect(isInboxNoiseMessage('Hello world')).toBe(false); }); }); + +describe('isMeaningfulBootstrapCheckInMessage', () => { + it('rejects idle_notification noise', () => { + expect( + isMeaningfulBootstrapCheckInMessage( + '{"type":"idle_notification","from":"alice","idleReason":"available"}' + ) + ).toBe(false); + }); + + it('accepts normal plain-text teammate replies', () => { + expect(isMeaningfulBootstrapCheckInMessage('Я на месте и готов продолжать.')).toBe(true); + }); + + it('rejects blank text', () => { + expect(isMeaningfulBootstrapCheckInMessage(' ')).toBe(false); + }); +});